aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_dispatch/routing
diff options
context:
space:
mode:
Diffstat (limited to 'actionpack/lib/action_dispatch/routing')
-rw-r--r--actionpack/lib/action_dispatch/routing/inspector.rb97
-rw-r--r--actionpack/lib/action_dispatch/routing/mapper.rb900
-rw-r--r--actionpack/lib/action_dispatch/routing/polymorphic_routes.rb270
-rw-r--r--actionpack/lib/action_dispatch/routing/redirection.rb46
-rw-r--r--actionpack/lib/action_dispatch/routing/route_set.rb389
-rw-r--r--actionpack/lib/action_dispatch/routing/routes_proxy.rb4
-rw-r--r--actionpack/lib/action_dispatch/routing/url_for.rb58
7 files changed, 951 insertions, 813 deletions
diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb
index 48c10a7d4c..b91ffb8419 100644
--- a/actionpack/lib/action_dispatch/routing/inspector.rb
+++ b/actionpack/lib/action_dispatch/routing/inspector.rb
@@ -1,5 +1,5 @@
-require 'delegate'
-require 'active_support/core_ext/string/strip'
+require "delegate"
+require "active_support/core_ext/string/strip"
module ActionDispatch
module Routing
@@ -16,10 +16,6 @@ module ActionDispatch
app.app
end
- def verb
- super.source.gsub(/[$^]/, '')
- end
-
def path
super.spec.to_s
end
@@ -37,15 +33,15 @@ module ActionDispatch
end
def controller
- requirements[:controller] || ':controller'
+ parts.include?(:controller) ? ":controller" : requirements[:controller]
end
def action
- requirements[:action] || ':action'
+ parts.include?(:action) ? ":action" : requirements[:action]
end
def internal?
- controller.to_s =~ %r{\Arails/(info|mailers|welcome)}
+ internal
end
def engine?
@@ -55,7 +51,7 @@ module ActionDispatch
##
# This class is just used for displaying route information when someone
- # executes `rake routes` or looks at the RoutingError page.
+ # executes `rails routes` or looks at the RoutingError page.
# People should not use this class.
class RoutesInspector # :nodoc:
def initialize(routes)
@@ -64,12 +60,10 @@ module ActionDispatch
end
def format(formatter, filter = nil)
- routes_to_display = filter_routes(filter)
-
+ routes_to_display = filter_routes(normalize_filter(filter))
routes = collect_routes(routes_to_display)
-
if routes.none?
- formatter.no_routes
+ formatter.no_routes(collect_routes(@routes))
return formatter.result
end
@@ -86,37 +80,48 @@ module ActionDispatch
private
- def filter_routes(filter)
- if filter
- @routes.select { |route| route.defaults[:controller] == filter }
- else
- @routes
+ def normalize_filter(filter)
+ if filter.is_a?(Hash) && filter[:controller]
+ { controller: /#{filter[:controller].downcase.sub(/_?controller\z/, '').sub('::', '/')}/ }
+ elsif filter
+ { controller: /#{filter}/, action: /#{filter}/, verb: /#{filter}/, name: /#{filter}/, path: /#{filter}/ }
+ end
+ end
+
+ def filter_routes(filter)
+ if filter
+ @routes.select do |route|
+ route_wrapper = RouteWrapper.new(route)
+ filter.any? { |default, value| route_wrapper.send(default) =~ value }
+ end
+ else
+ @routes
+ end
end
- end
- def collect_routes(routes)
- routes.collect do |route|
- RouteWrapper.new(route)
- end.reject(&:internal?).collect do |route|
- collect_engine_routes(route)
+ def collect_routes(routes)
+ routes.collect do |route|
+ RouteWrapper.new(route)
+ end.reject(&:internal?).collect do |route|
+ collect_engine_routes(route)
- { name: route.name,
- verb: route.verb,
- path: route.path,
- reqs: route.reqs }
+ { name: route.name,
+ verb: route.verb,
+ path: route.path,
+ reqs: route.reqs }
+ end
end
- end
- def collect_engine_routes(route)
- name = route.endpoint
- return unless route.engine?
- return if @engines[name]
+ def collect_engine_routes(route)
+ name = route.endpoint
+ return unless route.engine?
+ return if @engines[name]
- routes = route.rack_app.routes
- if routes.is_a?(ActionDispatch::Routing::RouteSet)
- @engines[name] = collect_routes(routes.routes)
+ routes = route.rack_app.routes
+ if routes.is_a?(ActionDispatch::Routing::RouteSet)
+ @engines[name] = collect_routes(routes.routes)
+ end
end
- end
end
class ConsoleFormatter
@@ -140,19 +145,23 @@ module ActionDispatch
@buffer << draw_header(routes)
end
- def no_routes
- @buffer << <<-MESSAGE.strip_heredoc
+ def no_routes(routes)
+ @buffer <<
+ if routes.none?
+ <<-MESSAGE.strip_heredoc
You don't have any routes defined!
Please add some routes in config/routes.rb.
-
- For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html.
MESSAGE
+ else
+ "No routes were found for this controller"
+ end
+ @buffer << "For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html."
end
private
def draw_section(routes)
- header_lengths = ['Prefix', 'Verb', 'URI Pattern'].map(&:length)
+ header_lengths = ["Prefix", "Verb", "URI Pattern"].map(&:length)
name_width, verb_width, path_width = widths(routes).zip(header_lengths).map(&:max)
routes.map do |r|
@@ -191,7 +200,7 @@ module ActionDispatch
def header(routes)
end
- def no_routes
+ def no_routes(*)
@buffer << <<-MESSAGE.strip_heredoc
<p>You don't have any routes defined!</p>
<ul>
diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb
index 15980db39b..4efde09b8b 100644
--- a/actionpack/lib/action_dispatch/routing/mapper.rb
+++ b/actionpack/lib/action_dispatch/routing/mapper.rb
@@ -1,22 +1,21 @@
-require 'active_support/core_ext/hash/except'
-require 'active_support/core_ext/hash/reverse_merge'
-require 'active_support/core_ext/hash/slice'
-require 'active_support/core_ext/enumerable'
-require 'active_support/core_ext/array/extract_options'
-require 'active_support/core_ext/module/remove_method'
-require 'active_support/inflector'
-require 'action_dispatch/routing/redirection'
-require 'action_dispatch/routing/endpoint'
+require "active_support/core_ext/hash/slice"
+require "active_support/core_ext/enumerable"
+require "active_support/core_ext/array/extract_options"
+require "action_dispatch/routing/redirection"
+require "action_dispatch/routing/endpoint"
module ActionDispatch
module Routing
class Mapper
URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port]
- class Constraints < Endpoint #:nodoc:
+ class Constraints < Routing::Endpoint #:nodoc:
attr_reader :app, :constraints
- def initialize(app, constraints, dispatcher_p)
+ SERVE = ->(app, req) { app.serve req }
+ CALL = ->(app, req) { app.call req.env }
+
+ def initialize(app, constraints, strategy)
# Unwrap Constraints objects. I don't actually think it's possible
# to pass a Constraints object to this constructor, but there were
# multiple places that kept testing children of this object. I
@@ -26,12 +25,12 @@ module ActionDispatch
app = app.app
end
- @dispatcher = dispatcher_p
+ @strategy = strategy
@app, @constraints, = app, constraints
end
- def dispatcher?; @dispatcher; end
+ def dispatcher?; @strategy == SERVE; end
def matches?(req)
@constraints.all? do |constraint|
@@ -41,13 +40,9 @@ module ActionDispatch
end
def serve(req)
- return [ 404, {'X-Cascade' => 'pass'}, [] ] unless matches?(req)
+ return [ 404, { "X-Cascade" => "pass" }, [] ] unless matches?(req)
- if dispatcher?
- @app.serve req
- else
- @app.call req.env
- end
+ @strategy.call @app, req
end
private
@@ -59,101 +54,180 @@ module ActionDispatch
class Mapping #:nodoc:
ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
- attr_reader :requirements, :conditions, :defaults
- attr_reader :to, :default_controller, :default_action, :as, :anchor
+ attr_reader :requirements, :defaults
+ attr_reader :to, :default_controller, :default_action
+ attr_reader :required_defaults, :ast
- def self.build(scope, set, path, as, options)
+ def self.build(scope, set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options)
options = scope[:options].merge(options) if scope[:options]
- options.delete :only
- options.delete :except
- options.delete :shallow_path
- options.delete :shallow_prefix
- options.delete :shallow
+ defaults = (scope[:defaults] || {}).dup
+ scope_constraints = scope[:constraints] || {}
+
+ new set, ast, defaults, controller, default_action, scope[:module], to, formatted, scope_constraints, scope[:blocks] || [], via, options_constraints, anchor, options
+ end
+
+ def self.check_via(via)
+ if via.empty?
+ msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \
+ "If you want to expose your action to both GET and POST, add `via: [:get, :post]` option.\n" \
+ "If you want to expose your action to GET, use `get` in the router:\n" \
+ " Instead of: match \"controller#action\"\n" \
+ " Do: get \"controller#action\""
+ raise ArgumentError, msg
+ end
+ via
+ end
+
+ def self.normalize_path(path, format)
+ path = Mapper.normalize_path(path)
- defaults = (scope[:defaults] || {}).merge options.delete(:defaults) || {}
+ if format == true
+ "#{path}.:format"
+ elsif optional_format?(path, format)
+ "#{path}(.:format)"
+ else
+ path
+ end
+ end
- new scope, set, path, defaults, as, options
+ def self.optional_format?(path, format)
+ format != false && !path.include?(":format") && !path.end_with?("/")
end
- def initialize(scope, set, path, defaults, as, options)
- @requirements, @conditions = {}, {}
+ def initialize(set, ast, defaults, controller, default_action, modyoule, to, formatted, scope_constraints, blocks, via, options_constraints, anchor, options)
@defaults = defaults
@set = set
- @to = options.delete :to
- @default_controller = options.delete(:controller) || scope[:controller]
- @default_action = options.delete(:action) || scope[:action]
- @as = as
- @anchor = options.delete :anchor
+ @to = to
+ @default_controller = controller
+ @default_action = default_action
+ @ast = ast
+ @anchor = anchor
+ @via = via
+ @internal = options.delete(:internal)
- formatted = options.delete :format
- via = Array(options.delete(:via) { [] })
- options_constraints = options.delete :constraints
+ path_params = ast.find_all(&:symbol?).map(&:to_sym)
- path = normalize_path! path, formatted
- ast = path_ast path
- path_params = path_params ast
+ options = add_wildcard_options(options, formatted, ast)
- options = normalize_options!(options, formatted, path_params, ast, scope[:module])
+ options = normalize_options!(options, path_params, modyoule)
+ split_options = constraints(options, path_params)
- split_constraints(path_params, scope[:constraints]) if scope[:constraints]
- constraints = constraints(options, path_params)
+ constraints = scope_constraints.merge Hash[split_options[:constraints] || []]
- split_constraints path_params, constraints
+ if options_constraints.is_a?(Hash)
+ @defaults = Hash[options_constraints.find_all { |key, default|
+ URL_OPTIONS.include?(key) && (String === default || Integer === default)
+ }].merge @defaults
+ @blocks = blocks
+ constraints.merge! options_constraints
+ else
+ @blocks = blocks(options_constraints)
+ end
- @blocks = blocks(options_constraints, scope[:blocks])
+ requirements, conditions = split_constraints path_params, constraints
+ verify_regexp_requirements requirements.map(&:last).grep(Regexp)
- if options_constraints.is_a?(Hash)
- split_constraints path_params, options_constraints
- options_constraints.each do |key, default|
- if URL_OPTIONS.include?(key) && (String === default || Fixnum === default)
- @defaults[key] ||= default
- end
- end
+ formats = normalize_format(formatted)
+
+ @requirements = formats[:requirements].merge Hash[requirements]
+ @conditions = Hash[conditions]
+ @defaults = formats[:defaults].merge(@defaults).merge(normalize_defaults(options))
+
+ if path_params.include?(:action) && !@requirements.key?(:action)
+ @defaults[:action] ||= "index"
end
- normalize_format!(formatted)
+ @required_defaults = (split_options[:required_defaults] || []).map(&:first)
+ end
- @conditions[:path_info] = path
- @conditions[:parsed_path_info] = ast
+ def make_route(name, precedence)
+ route = Journey::Route.new(name,
+ application,
+ path,
+ conditions,
+ required_defaults,
+ defaults,
+ request_method,
+ precedence,
+ @internal)
+
+ route
+ end
- add_request_method(via, @conditions)
- normalize_defaults!(options)
+ def application
+ app(@blocks)
end
- def to_route
- [ app(@blocks), conditions, requirements, defaults, as, anchor ]
+ def path
+ build_path @ast, requirements, @anchor
end
- private
+ def conditions
+ build_conditions @conditions, @set.request_class
+ end
- def normalize_path!(path, format)
- path = Mapper.normalize_path(path)
+ def build_conditions(current_conditions, request_class)
+ conditions = current_conditions.dup
- if format == true
- "#{path}.:format"
- elsif optional_format?(path, format)
- "#{path}(.:format)"
+ conditions.keep_if do |k, _|
+ request_class.public_method_defined?(k)
+ end
+ end
+ private :build_conditions
+
+ def request_method
+ @via.map { |x| Journey::Route.verb_matcher(x) }
+ end
+ private :request_method
+
+ JOINED_SEPARATORS = SEPARATORS.join # :nodoc:
+
+ def build_path(ast, requirements, anchor)
+ pattern = Journey::Path::Pattern.new(ast, requirements, JOINED_SEPARATORS, anchor)
+
+ # Find all the symbol nodes that are adjacent to literal nodes and alter
+ # the regexp so that Journey will partition them into custom routes.
+ ast.find_all { |node|
+ next unless node.cat?
+
+ if node.left.literal? && node.right.symbol?
+ symbol = node.right
+ elsif node.left.literal? && node.right.cat? && node.right.left.symbol?
+ symbol = node.right.left
+ elsif node.left.symbol? && node.right.literal?
+ symbol = node.left
+ elsif node.left.symbol? && node.right.cat? && node.right.left.literal?
+ symbol = node.left
else
- path
+ next
end
- end
- def optional_format?(path, format)
- format != false && !path.include?(':format') && !path.end_with?('/')
- end
+ if symbol
+ symbol.regexp = /(?:#{Regexp.union(symbol.regexp, '-')})+/
+ end
+ }
+
+ pattern
+ end
+ private :build_path
- def normalize_options!(options, formatted, path_params, path_ast, modyoule)
+ private
+ def add_wildcard_options(options, formatted, path_ast)
# Add a constraint for wildcard route to make it non-greedy and match the
# optional format part of the route by default
if formatted != false
- path_ast.grep(Journey::Nodes::Star) do |node|
- options[node.name.to_sym] ||= /.+?/
- end
+ path_ast.grep(Journey::Nodes::Star).each_with_object({}) { |node, hash|
+ hash[node.name.to_sym] ||= /.+?/
+ }.merge options
+ else
+ options
end
+ end
+ def normalize_options!(options, path_params, modyoule)
if path_params.include?(:controller)
raise ArgumentError, ":controller segment is not allowed within a namespace block" if modyoule
@@ -178,74 +252,54 @@ module ActionDispatch
end
def split_constraints(path_params, constraints)
- constraints.each_pair do |key, requirement|
- if path_params.include?(key) || key == :controller
- verify_regexp_requirement(requirement) if requirement.is_a?(Regexp)
- @requirements[key] = requirement
- else
- @conditions[key] = requirement
- end
- end
- end
-
- def normalize_format!(formatted)
- if formatted == true
- @requirements[:format] ||= /.+/
- elsif Regexp === formatted
- @requirements[:format] = formatted
- @defaults[:format] = nil
- elsif String === formatted
- @requirements[:format] = Regexp.compile(formatted)
- @defaults[:format] = formatted
+ constraints.partition do |key, requirement|
+ path_params.include?(key) || key == :controller
end
end
- def verify_regexp_requirement(requirement)
- if requirement.source =~ ANCHOR_CHARACTERS_REGEX
- raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
- end
-
- if requirement.multiline?
- raise ArgumentError, "Regexp multiline option is not allowed in routing requirements: #{requirement.inspect}"
+ def normalize_format(formatted)
+ case formatted
+ when true
+ { requirements: { format: /.+/ },
+ defaults: {} }
+ when Regexp
+ { requirements: { format: formatted },
+ defaults: { format: nil } }
+ when String
+ { requirements: { format: Regexp.compile(formatted) },
+ defaults: { format: formatted } }
+ else
+ { requirements: {}, defaults: {} }
end
end
- def normalize_defaults!(options)
- options.each_pair do |key, default|
- unless Regexp === default
- @defaults[key] = default
+ def verify_regexp_requirements(requirements)
+ requirements.each do |requirement|
+ if requirement.source =~ ANCHOR_CHARACTERS_REGEX
+ raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
end
- end
- end
- def verify_callable_constraint(callable_constraint)
- unless callable_constraint.respond_to?(:call) || callable_constraint.respond_to?(:matches?)
- raise ArgumentError, "Invalid constraint: #{callable_constraint.inspect} must respond to :call or :matches?"
+ if requirement.multiline?
+ raise ArgumentError, "Regexp multiline option is not allowed in routing requirements: #{requirement.inspect}"
+ end
end
end
- def add_request_method(via, conditions)
- return if via == [:all]
-
- if via.empty?
- msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \
- "If you want to expose your action to both GET and POST, add `via: [:get, :post]` option.\n" \
- "If you want to expose your action to GET, use `get` in the router:\n" \
- " Instead of: match \"controller#action\"\n" \
- " Do: get \"controller#action\""
- raise ArgumentError, msg
- end
-
- conditions[:request_method] = via.map { |m| m.to_s.dasherize.upcase }
+ def normalize_defaults(options)
+ Hash[options.reject { |_, default| Regexp === default }]
end
def app(blocks)
- if to.respond_to?(:call)
- Constraints.new(to, blocks, false)
- elsif blocks.any?
- Constraints.new(dispatcher(defaults), blocks, true)
+ if to.is_a?(Class) && to < ActionController::Metal
+ Routing::RouteSet::StaticDispatcher.new to
else
- dispatcher(defaults)
+ if to.respond_to?(:call)
+ Constraints.new(to, blocks, Constraints::CALL)
+ elsif blocks.any?
+ Constraints.new(dispatcher(defaults.key?(:controller)), blocks, Constraints::SERVE)
+ else
+ dispatcher(defaults.key?(:controller))
+ end
end
end
@@ -278,7 +332,7 @@ module ActionDispatch
def split_to(to)
if to =~ /#/
- to.split('#')
+ to.split("#")
else
[]
end
@@ -303,40 +357,29 @@ module ActionDispatch
yield
end
- def blocks(options_constraints, scope_blocks)
- if options_constraints && !options_constraints.is_a?(Hash)
- verify_callable_constraint(options_constraints)
- [options_constraints]
- else
- scope_blocks || []
+ def blocks(callable_constraint)
+ unless callable_constraint.respond_to?(:call) || callable_constraint.respond_to?(:matches?)
+ raise ArgumentError, "Invalid constraint: #{callable_constraint.inspect} must respond to :call or :matches?"
end
+ [callable_constraint]
end
def constraints(options, path_params)
- constraints = {}
- required_defaults = []
- options.each_pair do |key, option|
+ options.group_by do |key, option|
if Regexp === option
- constraints[key] = option
+ :constraints
else
- required_defaults << key unless path_params.include?(key)
+ if path_params.include?(key)
+ :path_params
+ else
+ :required_defaults
+ end
end
end
- @conditions[:required_defaults] = required_defaults
- constraints
- end
-
- def path_params(ast)
- ast.grep(Journey::Nodes::Symbol).map { |n| n.name.to_sym }
- end
-
- def path_ast(path)
- parser = Journey::Parser.new
- parser.parse path
end
- def dispatcher(defaults)
- @set.dispatcher defaults
+ def dispatcher(raise_on_name_error)
+ Routing::RouteSet::Dispatcher.new raise_on_name_error
end
end
@@ -354,23 +397,6 @@ module ActionDispatch
end
module Base
- # You can specify what Rails should route "/" to with the root method:
- #
- # root to: 'pages#main'
- #
- # For options, see +match+, as +root+ uses it internally.
- #
- # You can also pass a string which will expand
- #
- # root 'pages#main'
- #
- # You should put the root route at the top of <tt>config/routes.rb</tt>,
- # because this means it will be matched first. As this is the most popular route
- # of most Rails applications, this is beneficial.
- def root(options = {})
- match '/', { :as => :root, :via => :get }.merge!(options)
- end
-
# Matches a url pattern to one or more routes.
#
# You should not use the +match+ method in your router
@@ -443,6 +469,21 @@ module ActionDispatch
# dynamic segment used to generate the routes).
# You can access that segment from your controller using
# <tt>params[<:param>]</tt>.
+ # In your router:
+ #
+ # resources :user, param: :name
+ #
+ # You can override <tt>ActiveRecord::Base#to_param</tt> of a related
+ # model to construct a URL:
+ #
+ # class User < ActiveRecord::Base
+ # def to_param
+ # name
+ # end
+ # end
+ #
+ # user = User.find_by(name: 'Phusion')
+ # user_path(user) # => "/users/Phusion"
#
# [:path]
# The path prefix for the routes.
@@ -526,7 +567,7 @@ module ActionDispatch
# [:format]
# Allows you to specify the default value for optional +format+
# segment or disable it by supplying +false+.
- def match(path, options=nil)
+ def match(path, options = nil)
end
# Mount a Rack-based application to be used within the application.
@@ -551,17 +592,20 @@ module ActionDispatch
def mount(app, options = nil)
if options
path = options.delete(:at)
- else
- unless Hash === app
- raise ArgumentError, "must be called with mount point"
- end
-
+ elsif Hash === app
options = app
app, path = options.find { |k, _| k.respond_to?(:call) }
options.delete(app) if app
end
- raise "A rack application must be specified" unless path
+ raise ArgumentError, "A rack application must be specified" unless app.respond_to?(:call)
+ raise ArgumentError, <<-MSG.strip_heredoc unless path
+ Must be called with mount point
+
+ mount SomeRackApp, at: "some_route"
+ or
+ mount(SomeRackApp => "some_route")
+ MSG
rails_app = rails_app? app
options[:as] ||= app_name(app, rails_app)
@@ -569,7 +613,7 @@ module ActionDispatch
target_as = name_for_action(options[:as], path)
options[:via] ||= :all
- match(path, options.merge(:to => app, :anchor => false, :format => false))
+ match(path, options.merge(to: app, anchor: false, format: false))
define_generate_prefix(app, target_as) if rails_app
self
@@ -588,7 +632,7 @@ module ActionDispatch
# Query if the following named route was already defined.
def has_named_route?(name)
- @set.named_routes.routes[name.to_sym]
+ @set.named_routes.key? name
end
private
@@ -616,6 +660,7 @@ module ActionDispatch
super(options)
else
prefix_options = options.slice(*_route.segment_keys)
+ prefix_options[:relative_url_root] = "".freeze
# we must actually delete prefix segment keys to avoid passing them to next url_for
_route.segment_keys.each { |k| options.delete(k) }
_routes.url_helpers.send("#{name}_path", prefix_options)
@@ -764,7 +809,7 @@ module ActionDispatch
options = args.extract_options!.dup
scope = {}
- options[:path] = args.flatten.join('/') if args.any?
+ options[:path] = args.flatten.join("/") if args.any?
options[:constraints] ||= {}
unless nested_scope?
@@ -773,25 +818,34 @@ module ActionDispatch
end
if options[:constraints].is_a?(Hash)
- defaults = options[:constraints].select do
- |k, v| URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Fixnum))
+ defaults = options[:constraints].select do |k, v|
+ URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Integer))
end
- (options[:defaults] ||= {}).reverse_merge!(defaults)
+ options[:defaults] = defaults.merge(options[:defaults] || {})
else
block, options[:constraints] = options[:constraints], {}
end
+ if options.key?(:only) || options.key?(:except)
+ scope[:action_options] = { only: options.delete(:only),
+ except: options.delete(:except) }
+ end
+
+ if options.key? :anchor
+ raise ArgumentError, "anchor is ignored unless passed to `match`"
+ end
+
@scope.options.each do |option|
if option == :blocks
value = block
elsif option == :options
value = options
else
- value = options.delete(option)
+ value = options.delete(option) { POISON }
end
- if value
+ unless POISON == value
scope[option] = send("merge_#{option}_scope", @scope[option], value)
end
end
@@ -803,14 +857,18 @@ module ActionDispatch
@scope = @scope.parent
end
+ POISON = Object.new # :nodoc:
+
# Scopes routes to a specific controller
#
# controller "food" do
# match "bacon", action: :bacon, via: :get
# end
- def controller(controller, options={})
- options[:controller] = controller
- scope(options) { yield }
+ def controller(controller)
+ @scope = @scope.new(controller: controller)
+ yield
+ ensure
+ @scope = @scope.parent
end
# Scopes routes to a specific namespace. For example:
@@ -856,13 +914,14 @@ module ActionDispatch
defaults = {
module: path,
- path: options.fetch(:path, path),
as: options.fetch(:as, path),
shallow_path: options.fetch(:path, path),
shallow_prefix: options.fetch(:as, path)
}
- scope(defaults.merge!(options)) { yield }
+ path_scope(options.delete(:path) { path }) do
+ scope(defaults.merge!(options)) { yield }
+ end
end
# === Parameter Restriction
@@ -921,7 +980,7 @@ module ActionDispatch
# resources :iphones
# end
def constraints(constraints = {})
- scope(:constraints => constraints) { yield }
+ scope(constraints: constraints) { yield }
end
# Allows you to set default parameters for a route, such as this:
@@ -930,7 +989,10 @@ module ActionDispatch
# end
# Using this, the +:id+ parameter here will default to 'home'.
def defaults(defaults = {})
- scope(:defaults => defaults) { yield }
+ @scope = @scope.new(defaults: merge_defaults_scope(@scope[:defaults], defaults))
+ yield
+ ensure
+ @scope = @scope.parent
end
private
@@ -962,6 +1024,14 @@ module ActionDispatch
child
end
+ def merge_via_scope(parent, child) #:nodoc:
+ child
+ end
+
+ def merge_format_scope(parent, child) #:nodoc:
+ child
+ end
+
def merge_path_names_scope(parent, child) #:nodoc:
merge_options_scope(parent, child)
end
@@ -981,15 +1051,15 @@ module ActionDispatch
end
def merge_options_scope(parent, child) #:nodoc:
- (parent || {}).except(*override_keys(child)).merge!(child)
+ (parent || {}).merge(child)
end
def merge_shallow_scope(parent, child) #:nodoc:
child ? true : false
end
- def override_keys(child) #:nodoc:
- child.key?(:only) || child.key?(:except) ? [:only, :except] : []
+ def merge_to_scope(parent, child)
+ child
end
end
@@ -1040,27 +1110,34 @@ module ActionDispatch
CANONICAL_ACTIONS = %w(index create new show update destroy)
class Resource #:nodoc:
- attr_reader :controller, :path, :options, :param
+ attr_reader :controller, :path, :param
- def initialize(entities, options = {})
+ def initialize(entities, api_only, shallow, options = {})
@name = entities.to_s
@path = (options[:path] || @name).to_s
@controller = (options[:controller] || @name).to_s
@as = options[:as]
@param = (options[:param] || :id).to_sym
@options = options
- @shallow = false
+ @shallow = shallow
+ @api_only = api_only
+ @only = options.delete :only
+ @except = options.delete :except
end
def default_actions
- [:index, :create, :new, :show, :update, :destroy, :edit]
+ if @api_only
+ [:index, :create, :show, :update, :destroy]
+ else
+ [:index, :create, :new, :show, :update, :destroy, :edit]
+ end
end
def actions
- if only = @options[:only]
- Array(only).map(&:to_sym)
- elsif except = @options[:except]
- default_actions - Array(except).map(&:to_sym)
+ if @only
+ Array(@only).map(&:to_sym)
+ elsif @except
+ default_actions - Array(@except).map(&:to_sym)
else
default_actions
end
@@ -1087,7 +1164,7 @@ module ActionDispatch
end
def resource_scope
- { :controller => controller }
+ controller
end
alias :collection_scope :path
@@ -1110,17 +1187,15 @@ module ActionDispatch
"#{path}/:#{nested_param}"
end
- def shallow=(value)
- @shallow = value
- end
-
def shallow?
@shallow
end
+
+ def singleton?; false; end
end
class SingletonResource < Resource #:nodoc:
- def initialize(entities, options)
+ def initialize(entities, api_only, shallow, options)
super
@as = nil
@controller = (options[:controller] || plural).to_s
@@ -1128,7 +1203,11 @@ module ActionDispatch
end
def default_actions
- [:show, :create, :update, :destroy, :new, :edit]
+ if @api_only
+ [:show, :create, :update, :destroy]
+ else
+ [:show, :create, :update, :destroy, :new, :edit]
+ end
end
def plural
@@ -1144,6 +1223,8 @@ module ActionDispatch
alias :member_scope :path
alias :nested_scope :path
+
+ def singleton?; true; end
end
def resources_path_names(options)
@@ -1163,11 +1244,11 @@ module ActionDispatch
# the plural):
#
# GET /profile/new
- # POST /profile
# GET /profile
# GET /profile/edit
# PATCH/PUT /profile
# DELETE /profile
+ # POST /profile
#
# === Options
# Takes same options as +resources+.
@@ -1178,20 +1259,23 @@ module ActionDispatch
return self
end
- resource_scope(:resource, SingletonResource.new(resources.pop, options)) do
- yield if block_given?
+ with_scope_level(:resource) do
+ options = apply_action_options options
+ resource_scope(SingletonResource.new(resources.pop, api_only?, @scope[:shallow], options)) do
+ yield if block_given?
- concerns(options[:concerns]) if options[:concerns]
+ concerns(options[:concerns]) if options[:concerns]
- collection do
- post :create
- end if parent_resource.actions.include?(:create)
+ new do
+ get :new
+ end if parent_resource.actions.include?(:new)
- new do
- get :new
- end if parent_resource.actions.include?(:new)
+ set_member_mappings_for_resource
- set_member_mappings_for_resource
+ collection do
+ post :create
+ end if parent_resource.actions.include?(:create)
+ end
end
self
@@ -1336,21 +1420,24 @@ module ActionDispatch
return self
end
- resource_scope(:resources, Resource.new(resources.pop, options)) do
- yield if block_given?
+ with_scope_level(:resources) do
+ options = apply_action_options options
+ resource_scope(Resource.new(resources.pop, api_only?, @scope[:shallow], options)) do
+ yield if block_given?
- concerns(options[:concerns]) if options[:concerns]
+ concerns(options[:concerns]) if options[:concerns]
- collection do
- get :index if parent_resource.actions.include?(:index)
- post :create if parent_resource.actions.include?(:create)
- end
+ collection do
+ get :index if parent_resource.actions.include?(:index)
+ post :create if parent_resource.actions.include?(:create)
+ end
- new do
- get :new
- end if parent_resource.actions.include?(:new)
+ new do
+ get :new
+ end if parent_resource.actions.include?(:new)
- set_member_mappings_for_resource
+ set_member_mappings_for_resource
+ end
end
self
@@ -1374,7 +1461,7 @@ module ActionDispatch
end
with_scope_level(:collection) do
- scope(parent_resource.collection_scope) do
+ path_scope(parent_resource.collection_scope) do
yield
end
end
@@ -1398,9 +1485,11 @@ module ActionDispatch
with_scope_level(:member) do
if shallow?
- shallow_scope(parent_resource.member_scope) { yield }
+ shallow_scope {
+ path_scope(parent_resource.member_scope) { yield }
+ }
else
- scope(parent_resource.member_scope) { yield }
+ path_scope(parent_resource.member_scope) { yield }
end
end
end
@@ -1411,7 +1500,7 @@ module ActionDispatch
end
with_scope_level(:new) do
- scope(parent_resource.new_scope(action_path(:new))) do
+ path_scope(parent_resource.new_scope(action_path(:new))) do
yield
end
end
@@ -1424,9 +1513,15 @@ module ActionDispatch
with_scope_level(:nested) do
if shallow? && shallow_nesting_depth >= 1
- shallow_scope(parent_resource.nested_scope, nested_options) { yield }
+ shallow_scope do
+ path_scope(parent_resource.nested_scope) do
+ scope(nested_options) { yield }
+ end
+ end
else
- scope(parent_resource.nested_scope, nested_options) { yield }
+ path_scope(parent_resource.nested_scope) do
+ scope(nested_options) { yield }
+ end
end
end
end
@@ -1441,13 +1536,14 @@ module ActionDispatch
end
def shallow
- scope(:shallow => true) do
- yield
- end
+ @scope = @scope.new(shallow: true)
+ yield
+ ensure
+ @scope = @scope.parent
end
def shallow?
- parent_resource.instance_of?(Resource) && @scope[:shallow]
+ !parent_resource.singleton? && @scope[:shallow]
end
# Matches a url pattern to one or more routes.
@@ -1456,11 +1552,13 @@ module ActionDispatch
# match 'path' => 'controller#action', via: patch
# match 'path', to: 'controller#action', via: :post
# match 'path', 'otherpath', on: :member, via: :get
- def match(path, *rest)
+ def match(path, *rest, &block)
if rest.empty? && Hash === path
options = path
path, to = options.find { |name, _value| name.is_a?(String) }
+ raise ArgumentError, "Route path not specified" if path.nil?
+
case to
when Symbol
options[:action] = to
@@ -1481,77 +1579,30 @@ module ActionDispatch
paths = [path] + rest
end
- options[:anchor] = true unless options.key?(:anchor)
-
- if options[:on] && !VALID_ON_OPTIONS.include?(options[:on])
- raise ArgumentError, "Unknown scope #{on.inspect} given to :on"
- end
-
- if @scope[:controller] && @scope[:action]
- options[:to] ||= "#{@scope[:controller]}##{@scope[:action]}"
- end
-
- paths.each do |_path|
- route_options = options.dup
- route_options[:path] ||= _path if _path.is_a?(String)
-
- path_without_format = _path.to_s.sub(/\(\.:format\)$/, '')
- if using_match_shorthand?(path_without_format, route_options)
- route_options[:to] ||= path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1')
- route_options[:to].tr!("-", "_")
- end
-
- decomposed_match(_path, route_options)
- end
- self
- end
-
- def using_match_shorthand?(path, options)
- path && (options[:to] || options[:action]).nil? && path =~ %r{^/?[-\w]+/[-\w/]+$}
- end
-
- def decomposed_match(path, options) # :nodoc:
- if on = options.delete(:on)
- send(on) { decomposed_match(path, options) }
+ if options.key?(:defaults)
+ defaults(options.delete(:defaults)) { map_match(paths, options, &block) }
else
- case @scope.scope_level
- when :resources
- nested { decomposed_match(path, options) }
- when :resource
- member { decomposed_match(path, options) }
- else
- add_route(path, options)
- end
+ map_match(paths, options, &block)
end
end
- def add_route(action, options) # :nodoc:
- path = path_for_action(action, options.delete(:path))
- raise ArgumentError, "path is required" if path.blank?
-
- action = action.to_s.dup
-
- if action =~ /^[\w\-\/]+$/
- options[:action] ||= action.tr('-', '_') unless action.include?("/")
- else
- action = nil
- end
-
- as = if !options.fetch(:as, true) # if it's set to nil or false
- options.delete(:as)
- else
- name_for_action(options.delete(:as), action)
- end
-
- mapping = Mapping.build(@scope, @set, URI.parser.escape(path), as, options)
- app, conditions, requirements, defaults, as, anchor = mapping.to_route
- @set.add_route(app, conditions, requirements, defaults, as, anchor)
- end
-
- def root(path, options={})
+ # You can specify what Rails should route "/" to with the root method:
+ #
+ # root to: 'pages#main'
+ #
+ # For options, see +match+, as +root+ uses it internally.
+ #
+ # You can also pass a string which will expand
+ #
+ # root 'pages#main'
+ #
+ # You should put the root route at the top of <tt>config/routes.rb</tt>,
+ # because this means it will be matched first. As this is the most popular route
+ # of most Rails applications, this is beneficial.
+ def root(path, options = {})
if path.is_a?(String)
options[:to] = path
- elsif path.is_a?(Hash) and options.empty?
+ elsif path.is_a?(Hash) && options.empty?
options = path
else
raise ArgumentError, "must be called with a path and/or options"
@@ -1559,12 +1610,12 @@ module ActionDispatch
if @scope.resources?
with_scope_level(:root) do
- scope(parent_resource.path) do
- super(options)
+ path_scope(parent_resource.path) do
+ match_root_route(options)
end
end
else
- super(options)
+ match_root_route(options)
end
end
@@ -1604,23 +1655,20 @@ module ActionDispatch
return true
end
- unless action_options?(options)
- options.merge!(scope_action_options) if scope_action_options?
- end
-
false
end
- def action_options?(options) #:nodoc:
- options[:only] || options[:except]
+ def apply_action_options(options) # :nodoc:
+ return options if action_options? options
+ options.merge scope_action_options
end
- def scope_action_options? #:nodoc:
- @scope[:options] && (@scope[:options][:only] || @scope[:options][:except])
+ def action_options?(options) #:nodoc:
+ options[:only] || options[:except]
end
def scope_action_options #:nodoc:
- @scope[:options].slice(:only, :except)
+ @scope[:action_options] || {}
end
def resource_scope? #:nodoc:
@@ -1635,18 +1683,6 @@ module ActionDispatch
@scope.nested?
end
- def with_exclusive_scope
- begin
- @scope = @scope.new(:as => nil, :path => nil)
-
- with_scope_level(:exclusive) do
- yield
- end
- ensure
- @scope = @scope.parent
- end
- end
-
def with_scope_level(kind)
@scope = @scope.new_level(kind)
yield
@@ -1654,21 +1690,16 @@ module ActionDispatch
@scope = @scope.parent
end
- def resource_scope(kind, resource) #:nodoc:
- resource.shallow = @scope[:shallow]
- @scope = @scope.new(:scope_level_resource => resource)
- @nesting.push(resource)
+ def resource_scope(resource) #:nodoc:
+ @scope = @scope.new(scope_level_resource: resource)
- with_scope_level(kind) do
- scope(parent_resource.resource_scope) { yield }
- end
+ controller(resource.resource_scope) { yield }
ensure
- @nesting.pop
@scope = @scope.parent
end
def nested_options #:nodoc:
- options = { :as => parent_resource.member_name }
+ options = { as: parent_resource.member_name }
options[:constraints] = {
parent_resource.nested_param => param_constraint
} if param_constraint?
@@ -1676,12 +1707,10 @@ module ActionDispatch
options
end
- def nesting_depth #:nodoc:
- @nesting.size
- end
-
def shallow_nesting_depth #:nodoc:
- @nesting.count(&:shallow?)
+ @scope.find_all { |node|
+ node.frame[:scope_level_resource]
+ }.count { |node| node.frame[:scope_level_resource].shallow? }
end
def param_constraint? #:nodoc:
@@ -1696,27 +1725,28 @@ module ActionDispatch
resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
end
- def shallow_scope(path, options = {}) #:nodoc:
- scope = { :as => @scope[:shallow_prefix],
- :path => @scope[:shallow_path] }
+ def shallow_scope #:nodoc:
+ scope = { as: @scope[:shallow_prefix],
+ path: @scope[:shallow_path] }
@scope = @scope.new scope
- scope(path, options) { yield }
+ yield
ensure
@scope = @scope.parent
end
def path_for_action(action, path) #:nodoc:
- if path.blank? && canonical_action?(action)
+ return "#{@scope[:path]}/#{path}" if path
+
+ if canonical_action?(action)
@scope[:path].to_s
else
- "#{@scope[:path]}/#{action_path(action, path)}"
+ "#{@scope[:path]}/#{action_path(action)}"
end
end
- def action_path(name, path = nil) #:nodoc:
- name = name.to_sym if name.is_a?(String)
- path || @scope[:path_names][name] || name.to_s
+ def action_path(name) #:nodoc:
+ @scope[:path_names][name.to_sym] || name
end
def prefix_name_for_action(as, action) #:nodoc:
@@ -1726,8 +1756,8 @@ module ActionDispatch
prefix = action
end
- if prefix && prefix != '/' && !prefix.empty?
- Mapper.normalize_name prefix.to_s.tr('-', '_')
+ if prefix && prefix != "/" && !prefix.empty?
+ Mapper.normalize_name prefix.to_s.tr("-", "_")
end
end
@@ -1743,14 +1773,14 @@ module ActionDispatch
end
action_name = @scope.action_name(name_prefix, prefix, collection_name, member_name)
- candidate = action_name.select(&:present?).join('_')
+ candidate = action_name.select(&:present?).join("_")
unless candidate.empty?
# If a name was not explicitly given, we check if it is valid
# and return nil in case it isn't. Otherwise, we pass the invalid name
# forward so the underlying router engine treats it and raises an exception.
if as.nil?
- candidate unless candidate !~ /\A[_a-z]/i || @set.named_routes.key?(candidate)
+ candidate unless candidate !~ /\A[_a-z]/i || has_named_route?(candidate)
else
candidate
end
@@ -1768,6 +1798,124 @@ module ActionDispatch
delete :destroy if parent_resource.actions.include?(:destroy)
end
end
+
+ def api_only?
+ @set.api_only?
+ end
+
+ private
+
+ def path_scope(path)
+ @scope = @scope.new(path: merge_path_scope(@scope[:path], path))
+ yield
+ ensure
+ @scope = @scope.parent
+ end
+
+ def map_match(paths, options)
+ if options[:on] && !VALID_ON_OPTIONS.include?(options[:on])
+ raise ArgumentError, "Unknown scope #{on.inspect} given to :on"
+ end
+
+ if @scope[:to]
+ options[:to] ||= @scope[:to]
+ end
+
+ if @scope[:controller] && @scope[:action]
+ options[:to] ||= "#{@scope[:controller]}##{@scope[:action]}"
+ end
+
+ controller = options.delete(:controller) || @scope[:controller]
+ option_path = options.delete :path
+ to = options.delete :to
+ via = Mapping.check_via Array(options.delete(:via) {
+ @scope[:via]
+ })
+ formatted = options.delete(:format) { @scope[:format] }
+ anchor = options.delete(:anchor) { true }
+ options_constraints = options.delete(:constraints) || {}
+
+ path_types = paths.group_by(&:class)
+ path_types.fetch(String, []).each do |_path|
+ route_options = options.dup
+ if _path && option_path
+ raise ArgumentError, "Ambigous route definition. Both :path and the route path where specified as strings."
+ end
+ to = get_to_from_path(_path, to, route_options[:action])
+ decomposed_match(_path, controller, route_options, _path, to, via, formatted, anchor, options_constraints)
+ end
+
+ path_types.fetch(Symbol, []).each do |action|
+ route_options = options.dup
+ decomposed_match(action, controller, route_options, option_path, to, via, formatted, anchor, options_constraints)
+ end
+
+ self
+ end
+
+ def get_to_from_path(path, to, action)
+ return to if to || action
+
+ path_without_format = path.sub(/\(\.:format\)$/, "")
+ if using_match_shorthand?(path_without_format)
+ path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1').tr("-", "_")
+ else
+ nil
+ end
+ end
+
+ def using_match_shorthand?(path)
+ path =~ %r{^/?[-\w]+/[-\w/]+$}
+ end
+
+ def decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) # :nodoc:
+ if on = options.delete(:on)
+ send(on) { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
+ else
+ case @scope.scope_level
+ when :resources
+ nested { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
+ when :resource
+ member { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
+ else
+ add_route(path, controller, options, _path, to, via, formatted, anchor, options_constraints)
+ end
+ end
+ end
+
+ def add_route(action, controller, options, _path, to, via, formatted, anchor, options_constraints) # :nodoc:
+ path = path_for_action(action, _path)
+ raise ArgumentError, "path is required" if path.blank?
+
+ action = action.to_s
+
+ default_action = options.delete(:action) || @scope[:action]
+
+ if action =~ /^[\w\-\/]+$/
+ default_action ||= action.tr("-", "_") unless action.include?("/")
+ else
+ action = nil
+ end
+
+ as = if !options.fetch(:as, true) # if it's set to nil or false
+ options.delete(:as)
+ else
+ name_for_action(options.delete(:as), action)
+ end
+
+ path = Mapping.normalize_path URI.parser.escape(path), formatted
+ ast = Journey::Parser.parse path
+
+ mapping = Mapping.build(@scope, @set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options)
+ @set.add_route(mapping, ast, as, anchor)
+ end
+
+ def match_root_route(options)
+ name = has_named_route?(name_for_action(:root, nil)) ? nil : :root
+ args = ["/", { as: name, via: :get }.merge!(options)]
+
+ match(*args)
+ end
end
# Routing Concerns allow you to declare common routes that can be reused
@@ -1878,14 +2026,14 @@ module ActionDispatch
class Scope # :nodoc:
OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module,
:controller, :action, :path_names, :constraints,
- :shallow, :blocks, :defaults, :options]
+ :shallow, :blocks, :defaults, :via, :format, :options, :to]
RESOURCE_SCOPES = [:resource, :resources]
RESOURCE_METHOD_SCOPES = [:collection, :member, :new]
attr_reader :parent, :scope_level
- def initialize(hash, parent = {}, scope_level = nil)
+ def initialize(hash, parent = NULL, scope_level = nil)
@hash = hash
@parent = parent
@scope_level = scope_level
@@ -1933,27 +2081,33 @@ module ActionDispatch
end
def new_level(level)
- self.class.new(self, self, level)
- end
-
- def fetch(key, &block)
- @hash.fetch(key, &block)
+ self.class.new(frame, self, level)
end
def [](key)
- @hash.fetch(key) { @parent[key] }
+ scope = find { |node| node.frame.key? key }
+ scope && scope.frame[key]
end
- def []=(k,v)
- @hash[k] = v
+ include Enumerable
+
+ def each
+ node = self
+ until node.equal? NULL
+ yield node
+ node = node.parent
+ end
end
+
+ def frame; @hash; end
+
+ NULL = Scope.new(nil, nil)
end
def initialize(set) #:nodoc:
@set = set
- @scope = Scope.new({ :path_names => @set.resources_path_names })
+ @scope = Scope.new(path_names: @set.resources_path_names)
@concerns = {}
- @nesting = []
end
include Base
diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
index 9934f5547a..432b9bf4c1 100644
--- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
+++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
@@ -4,7 +4,7 @@ module ActionDispatch
# given an Active Record model instance. They are to be used in combination with
# ActionController::Resources.
#
- # These methods are useful when you want to generate correct URL or path to a RESTful
+ # These methods are useful when you want to generate the correct URL or path to a RESTful
# resource without having to know the exact type of the record in question.
#
# Nested resources and/or namespaces are also supported, as illustrated in the example:
@@ -79,7 +79,7 @@ module ActionDispatch
# polymorphic_url([blog, post], anchor: 'my_anchor', script_name: "/my_app")
# # => "http://example.com/my_app/blogs/1/posts/1#my_anchor"
#
- # For all of these options, see the documentation for <tt>url_for</tt>.
+ # For all of these options, see the documentation for {url_for}[rdoc-ref:ActionDispatch::Routing::UrlFor].
#
# ==== Functionality
#
@@ -134,7 +134,6 @@ module ActionDispatch
opts
end
-
%w(edit new).each do |action|
module_eval <<-EOT, __FILE__, __LINE__ + 1
def #{action}_polymorphic_url(record_or_hash, options = {})
@@ -149,176 +148,175 @@ module ActionDispatch
private
- def polymorphic_url_for_action(action, record_or_hash, options)
- polymorphic_url(record_or_hash, options.merge(:action => action))
- end
+ def polymorphic_url_for_action(action, record_or_hash, options)
+ polymorphic_url(record_or_hash, options.merge(action: action))
+ end
- def polymorphic_path_for_action(action, record_or_hash, options)
- polymorphic_path(record_or_hash, options.merge(:action => action))
- end
+ def polymorphic_path_for_action(action, record_or_hash, options)
+ polymorphic_path(record_or_hash, options.merge(action: action))
+ end
- class HelperMethodBuilder # :nodoc:
- CACHE = { 'path' => {}, 'url' => {} }
+ class HelperMethodBuilder # :nodoc:
+ CACHE = { "path" => {}, "url" => {} }
- def self.get(action, type)
- type = type.to_s
- CACHE[type].fetch(action) { build action, type }
- end
+ def self.get(action, type)
+ type = type.to_s
+ CACHE[type].fetch(action) { build action, type }
+ end
- def self.url; CACHE['url'.freeze][nil]; end
- def self.path; CACHE['path'.freeze][nil]; end
+ def self.url; CACHE["url".freeze][nil]; end
+ def self.path; CACHE["path".freeze][nil]; end
- def self.build(action, type)
- prefix = action ? "#{action}_" : ""
- suffix = type
- if action.to_s == 'new'
- HelperMethodBuilder.singular prefix, suffix
- else
- HelperMethodBuilder.plural prefix, suffix
+ def self.build(action, type)
+ prefix = action ? "#{action}_" : ""
+ suffix = type
+ if action.to_s == "new"
+ HelperMethodBuilder.singular prefix, suffix
+ else
+ HelperMethodBuilder.plural prefix, suffix
+ end
end
- end
- def self.singular(prefix, suffix)
- new(->(name) { name.singular_route_key }, prefix, suffix)
- end
+ def self.singular(prefix, suffix)
+ new(->(name) { name.singular_route_key }, prefix, suffix)
+ end
- def self.plural(prefix, suffix)
- new(->(name) { name.route_key }, prefix, suffix)
- end
+ def self.plural(prefix, suffix)
+ new(->(name) { name.route_key }, prefix, suffix)
+ end
- def self.polymorphic_method(recipient, record_or_hash_or_array, action, type, options)
- builder = get action, type
+ def self.polymorphic_method(recipient, record_or_hash_or_array, action, type, options)
+ builder = get action, type
+
+ case record_or_hash_or_array
+ when Array
+ record_or_hash_or_array = record_or_hash_or_array.compact
+ if record_or_hash_or_array.empty?
+ raise ArgumentError, "Nil location provided. Can't build URI."
+ end
+ if record_or_hash_or_array.first.is_a?(ActionDispatch::Routing::RoutesProxy)
+ recipient = record_or_hash_or_array.shift
+ end
+
+ method, args = builder.handle_list record_or_hash_or_array
+ when String, Symbol
+ method, args = builder.handle_string record_or_hash_or_array
+ when Class
+ method, args = builder.handle_class record_or_hash_or_array
- case record_or_hash_or_array
- when Array
- record_or_hash_or_array = record_or_hash_or_array.compact
- if record_or_hash_or_array.empty?
+ when nil
raise ArgumentError, "Nil location provided. Can't build URI."
+ else
+ method, args = builder.handle_model record_or_hash_or_array
end
- if record_or_hash_or_array.first.is_a?(ActionDispatch::Routing::RoutesProxy)
- recipient = record_or_hash_or_array.shift
- end
-
- method, args = builder.handle_list record_or_hash_or_array
- when String, Symbol
- method, args = builder.handle_string record_or_hash_or_array
- when Class
- method, args = builder.handle_class record_or_hash_or_array
- when nil
- raise ArgumentError, "Nil location provided. Can't build URI."
- else
- method, args = builder.handle_model record_or_hash_or_array
+ if options.empty?
+ recipient.send(method, *args)
+ else
+ recipient.send(method, *args, options)
+ end
end
+ attr_reader :suffix, :prefix
- if options.empty?
- recipient.send(method, *args)
- else
- recipient.send(method, *args, options)
+ def initialize(key_strategy, prefix, suffix)
+ @key_strategy = key_strategy
+ @prefix = prefix
+ @suffix = suffix
end
- end
-
- attr_reader :suffix, :prefix
-
- def initialize(key_strategy, prefix, suffix)
- @key_strategy = key_strategy
- @prefix = prefix
- @suffix = suffix
- end
-
- def handle_string(record)
- [get_method_for_string(record), []]
- end
-
- def handle_string_call(target, str)
- target.send get_method_for_string str
- end
- def handle_class(klass)
- [get_method_for_class(klass), []]
- end
+ def handle_string(record)
+ [get_method_for_string(record), []]
+ end
- def handle_class_call(target, klass)
- target.send get_method_for_class klass
- end
+ def handle_string_call(target, str)
+ target.send get_method_for_string str
+ end
- def handle_model(record)
- args = []
+ def handle_class(klass)
+ [get_method_for_class(klass), []]
+ end
- model = record.to_model
- named_route = if model.persisted?
- args << model
- get_method_for_string model.model_name.singular_route_key
- else
- get_method_for_class model
- end
+ def handle_class_call(target, klass)
+ target.send get_method_for_class klass
+ end
- [named_route, args]
- end
+ def handle_model(record)
+ args = []
- def handle_model_call(target, model)
- method, args = handle_model model
- target.send(method, *args)
- end
+ model = record.to_model
+ named_route = if model.persisted?
+ args << model
+ get_method_for_string model.model_name.singular_route_key
+ else
+ get_method_for_class model
+ end
- def handle_list(list)
- record_list = list.dup
- record = record_list.pop
+ [named_route, args]
+ end
- args = []
+ def handle_model_call(target, model)
+ method, args = handle_model model
+ target.send(method, *args)
+ end
- route = record_list.map { |parent|
- case parent
+ def handle_list(list)
+ record_list = list.dup
+ record = record_list.pop
+
+ args = []
+
+ route = record_list.map { |parent|
+ case parent
+ when Symbol, String
+ parent.to_s
+ when Class
+ args << parent
+ parent.model_name.singular_route_key
+ else
+ args << parent.to_model
+ parent.to_model.model_name.singular_route_key
+ end
+ }
+
+ route <<
+ case record
when Symbol, String
- parent.to_s
+ record.to_s
when Class
- args << parent
- parent.model_name.singular_route_key
+ @key_strategy.call record.model_name
else
- args << parent.to_model
- parent.to_model.model_name.singular_route_key
+ model = record.to_model
+ if model.persisted?
+ args << model
+ model.model_name.singular_route_key
+ else
+ @key_strategy.call model.model_name
+ end
end
- }
-
- route <<
- case record
- when Symbol, String
- record.to_s
- when Class
- @key_strategy.call record.model_name
- else
- model = record.to_model
- if model.persisted?
- args << model
- model.model_name.singular_route_key
- else
- @key_strategy.call model.model_name
- end
- end
- route << suffix
+ route << suffix
- named_route = prefix + route.join("_")
- [named_route, args]
- end
+ named_route = prefix + route.join("_")
+ [named_route, args]
+ end
- private
+ private
- def get_method_for_class(klass)
- name = @key_strategy.call klass.model_name
- get_method_for_string name
- end
+ def get_method_for_class(klass)
+ name = @key_strategy.call klass.model_name
+ get_method_for_string name
+ end
- def get_method_for_string(str)
- "#{prefix}#{str}_#{suffix}"
- end
+ def get_method_for_string(str)
+ "#{prefix}#{str}_#{suffix}"
+ end
- [nil, 'new', 'edit'].each do |action|
- CACHE['url'][action] = build action, 'url'
- CACHE['path'][action] = build action, 'path'
+ [nil, "new", "edit"].each do |action|
+ CACHE["url"][action] = build action, "url"
+ CACHE["path"][action] = build action, "path"
+ end
end
- end
end
end
end
diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb
index 3c1c4fadf6..4e2318a45e 100644
--- a/actionpack/lib/action_dispatch/routing/redirection.rb
+++ b/actionpack/lib/action_dispatch/routing/redirection.rb
@@ -1,9 +1,9 @@
-require 'action_dispatch/http/request'
-require 'active_support/core_ext/uri'
-require 'active_support/core_ext/array/extract_options'
-require 'rack/utils'
-require 'action_controller/metal/exceptions'
-require 'action_dispatch/routing/endpoint'
+require "action_dispatch/http/request"
+require "active_support/core_ext/uri"
+require "active_support/core_ext/array/extract_options"
+require "rack/utils"
+require "action_controller/metal/exceptions"
+require "action_dispatch/routing/endpoint"
module ActionDispatch
module Routing
@@ -22,9 +22,8 @@ module ActionDispatch
end
def serve(req)
- req.check_path_parameters!
uri = URI.parse(path(req.path_parameters, req))
-
+
unless uri.host
if relative_path?(uri.path)
uri.path = "#{req.script_name}/#{uri.path}"
@@ -32,7 +31,7 @@ module ActionDispatch
uri.path = req.script_name.empty? ? "/" : req.script_name
end
end
-
+
uri.scheme ||= req.scheme
uri.host ||= req.host
uri.port ||= req.port unless req.standard_port?
@@ -40,9 +39,9 @@ module ActionDispatch
body = %(<html><body>You are being <a href="#{ERB::Util.unwrapped_html_escape(uri.to_s)}">redirected</a>.</body></html>)
headers = {
- 'Location' => uri.to_s,
- 'Content-Type' => 'text/html',
- 'Content-Length' => body.length.to_s
+ "Location" => uri.to_s,
+ "Content-Type" => "text/html",
+ "Content-Length" => body.length.to_s
}
[ status, headers, [body] ]
@@ -58,19 +57,19 @@ module ActionDispatch
private
def relative_path?(path)
- path && !path.empty? && path[0] != '/'
+ path && !path.empty? && path[0] != "/"
end
def escape(params)
- Hash[params.map{ |k,v| [k, Rack::Utils.escape(v)] }]
+ Hash[params.map { |k, v| [k, Rack::Utils.escape(v)] }]
end
def escape_fragment(params)
- Hash[params.map{ |k,v| [k, Journey::Router::Utils.escape_fragment(v)] }]
+ Hash[params.map { |k, v| [k, Journey::Router::Utils.escape_fragment(v)] }]
end
def escape_path(params)
- Hash[params.map{ |k,v| [k, Journey::Router::Utils.escape_path(v)] }]
+ Hash[params.map { |k, v| [k, Journey::Router::Utils.escape_path(v)] }]
end
end
@@ -104,11 +103,11 @@ module ActionDispatch
def path(params, request)
url_options = {
- :protocol => request.protocol,
- :host => request.host,
- :port => request.optional_port,
- :path => request.path,
- :params => request.query_parameters
+ protocol: request.protocol,
+ host: request.host,
+ port: request.optional_port,
+ path: request.path,
+ params: request.query_parameters
}.merge! options
if !params.empty? && url_options[:path].match(/%\{\w*\}/)
@@ -124,17 +123,16 @@ module ActionDispatch
url_options[:script_name] = request.script_name
end
end
-
+
ActionDispatch::Http::URL.url_for url_options
end
def inspect
- "redirect(#{status}, #{options.map{ |k,v| "#{k}: #{v}" }.join(', ')})"
+ "redirect(#{status}, #{options.map { |k, v| "#{k}: #{v}" }.join(', ')})"
end
end
module Redirection
-
# Redirect any path to another path:
#
# get "/stories" => redirect("/posts")
diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb
index d0d8ded515..5853adb110 100644
--- a/actionpack/lib/action_dispatch/routing/route_set.rb
+++ b/actionpack/lib/action_dispatch/routing/route_set.rb
@@ -1,14 +1,11 @@
-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'
-require 'action_dispatch/routing/endpoint'
+require "action_dispatch/journey"
+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"
+require "action_dispatch/routing/endpoint"
module ActionDispatch
module Routing
@@ -21,65 +18,47 @@ module ActionDispatch
alias inspect to_s
class Dispatcher < Routing::Endpoint
- def initialize(defaults)
- @defaults = defaults
- @controller_class_names = ThreadSafe::Cache.new
+ def initialize(raise_on_name_error)
+ @raise_on_name_error = raise_on_name_error
end
def dispatcher?; true; end
def serve(req)
- req.check_path_parameters!
- params = req.path_parameters
-
- prepare_params!(params)
-
- # Just raise undefined constant errors if a controller was specified as default.
- unless controller = controller(params, @defaults.key?(:controller))
- return [404, {'X-Cascade' => 'pass'}, []]
+ params = req.path_parameters
+ controller = controller req
+ res = controller.make_response! req
+ dispatch(controller, params[:action], req, res)
+ rescue ActionController::RoutingError
+ if @raise_on_name_error
+ raise
+ else
+ return [404, { "X-Cascade" => "pass" }, []]
end
-
- dispatch(controller, params[:action], req.env)
end
- def prepare_params!(params)
- normalize_controller!(params)
- merge_default_action!(params)
- end
+ private
- # If this is a default_controller (i.e. a controller specified by the user)
- # we should raise an error in case it's not found, because it usually means
- # a user error. However, if the controller was retrieved through a dynamic
- # segment, as in :controller(/:action), we should simply return nil and
- # delegate the control back to Rack cascade. Besides, if this is not a default
- # controller, it means we should respect the @scope[:module] parameter.
- def controller(params, default_controller=true)
- if params && params.key?(:controller)
- controller_param = params[:controller]
- controller_reference(controller_param)
- end
+ def controller(req)
+ req.controller_class
rescue NameError => e
- raise ActionController::RoutingError, e.message, e.backtrace if default_controller
+ raise ActionController::RoutingError, e.message, e.backtrace
end
- private
-
- def controller_reference(controller_param)
- const_name = @controller_class_names[controller_param] ||= "#{controller_param.camelize}Controller"
- ActiveSupport::Dependencies.constantize(const_name)
+ def dispatch(controller, action, req, res)
+ controller.dispatch(action, req, res)
end
+ end
- def dispatch(controller, action, env)
- controller.action(action).call(env)
+ class StaticDispatcher < Dispatcher
+ def initialize(controller_class)
+ super(false)
+ @controller_class = controller_class
end
- def normalize_controller!(params)
- params[:controller] = params[:controller].underscore if params.key?(:controller)
- end
+ private
- def merge_default_action!(params)
- params[:action] ||= 'index'
- end
+ def controller(_); @controller_class; end
end
# A NamedRouteCollection instance is a collection of named routes, and also
@@ -88,9 +67,10 @@ module ActionDispatch
class NamedRouteCollection
include Enumerable
attr_reader :routes, :url_helpers_module, :path_helpers_module
+ private :routes
def initialize
- @routes = {}
+ @routes = {}
@path_helpers = Set.new
@url_helpers = Set.new
@url_helpers_module = Module.new
@@ -142,6 +122,7 @@ module ActionDispatch
end
def key?(name)
+ return unless name
routes.key? name.to_sym
end
@@ -198,40 +179,40 @@ module ActionDispatch
private
- def optimized_helper(args)
- params = parameterize_args(args) { |k|
- raise_generation_error(args)
- }
+ def optimized_helper(args)
+ params = parameterize_args(args) do
+ raise_generation_error(args)
+ end
- @route.format params
- end
+ @route.format params
+ end
- def optimize_routes_generation?(t)
- t.send(:optimize_routes_generation?)
- end
+ def optimize_routes_generation?(t)
+ t.send(:optimize_routes_generation?)
+ end
- def parameterize_args(args)
- params = {}
- @arg_size.times { |i|
- key = @required_parts[i]
- value = args[i].to_param
- yield key if value.nil? || value.empty?
- params[key] = value
- }
- params
- end
+ def parameterize_args(args)
+ params = {}
+ @arg_size.times { |i|
+ key = @required_parts[i]
+ value = args[i].to_param
+ yield key if value.nil? || value.empty?
+ params[key] = value
+ }
+ params
+ end
- def raise_generation_error(args)
- missing_keys = []
- params = parameterize_args(args) { |missing_key|
- missing_keys << missing_key
- }
- constraints = Hash[@route.requirements.merge(params).sort_by{|k,v| k.to_s}]
- message = "No route matches #{constraints.inspect}"
- message << " missing required keys: #{missing_keys.sort.inspect}"
+ def raise_generation_error(args)
+ missing_keys = []
+ params = parameterize_args(args) { |missing_key|
+ missing_keys << missing_key
+ }
+ constraints = Hash[@route.requirements.merge(params).sort_by { |k, v| k.to_s }]
+ message = "No route matches #{constraints.inspect}"
+ message << ", missing required keys: #{missing_keys.sort.inspect}"
- raise ActionController::UrlGenerationError, message
- end
+ raise ActionController::UrlGenerationError, message
+ end
end
def initialize(route, options, route_name, url_strategy)
@@ -267,9 +248,13 @@ module ActionDispatch
path_params -= controller_options.keys
path_params -= result.keys
end
- path_params -= inner_options.keys
- path_params.take(args.size).each do |param|
- result[param] = args.shift
+ inner_options.each_key do |key|
+ path_params.delete(key)
+ end
+
+ args.each_with_index do |arg, index|
+ param = path_params[index]
+ result[param] = arg if param
end
end
@@ -278,29 +263,39 @@ module ActionDispatch
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(mod, route, name, opts, route_key, url_strategy)
- helper = UrlHelper.create(route, opts, route_key, url_strategy)
- mod.module_eval do
- define_method(name) do |*args|
- options = nil
- options = args.pop if args.last.is_a? Hash
- helper.call self, args, options
+ # 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(mod, route, name, opts, route_key, url_strategy)
+ helper = UrlHelper.create(route, opts, route_key, url_strategy)
+ mod.module_eval do
+ define_method(name) do |*args|
+ last = args.last
+ options = \
+ case last
+ when Hash
+ args.pop
+ when ActionController::Parameters
+ if last.permitted?
+ args.pop.to_h
+ else
+ raise ArgumentError, ActionDispatch::Routing::INSECURE_URL_PARAMETERS_MESSAGE
+ end
+ end
+ helper.call self, args, options
+ end
end
end
- end
end
# strategy for building urls to send to the client
@@ -315,21 +310,27 @@ module ActionDispatch
alias :routes :set
def self.default_resources_path_names
- { :new => 'new', :edit => 'edit' }
+ { new: "new", edit: "edit" }
end
def self.new_with_config(config)
+ route_set_config = DEFAULT_CONFIG
+
+ # engines apparently don't have this set
if config.respond_to? :relative_url_root
- new Config.new config.relative_url_root
- else
- # engines apparently don't have this set
- new
+ route_set_config.relative_url_root = config.relative_url_root
+ end
+
+ if config.respond_to? :api_only
+ route_set_config.api_only = config.api_only
end
+
+ new route_set_config
end
- Config = Struct.new :relative_url_root
+ Config = Struct.new :relative_url_root, :api_only
- DEFAULT_CONFIG = Config.new(nil)
+ DEFAULT_CONFIG = Config.new(nil, false)
def initialize(config = DEFAULT_CONFIG)
self.named_routes = NamedRouteCollection.new
@@ -345,17 +346,26 @@ module ActionDispatch
@set = Journey::Routes.new
@router = Journey::Router.new @set
- @formatter = Journey::Formatter.new @set
+ @formatter = Journey::Formatter.new self
end
def relative_url_root
@config.relative_url_root
end
+ def api_only?
+ @config.api_only
+ end
+
def request_class
ActionDispatch::Request
end
+ def make_request(env)
+ request_class.new env
+ end
+ private :make_request
+
def draw(&block)
clear! unless @disable_clear_and_finalize
eval_block(block)
@@ -372,10 +382,6 @@ module ActionDispatch
end
def eval_block(block)
- if block.arity == 1
- raise "You are using the old router DSL which has been removed in Rails 3.1. " <<
- "Please check how to update your routes file at: http://www.engineyard.com/blog/2010/the-lowdown-on-routes-in-rails-3/"
- end
mapper = Mapper.new(self)
if default_scope
mapper.with_default_scope(default_scope, &block)
@@ -399,10 +405,6 @@ module ActionDispatch
@prepend.each { |blk| eval_block(blk) }
end
- def dispatcher(defaults)
- Routing::RouteSet::Dispatcher.new(defaults)
- end
-
module MountedHelpers
extend ActiveSupport::Concern
include UrlFor
@@ -498,7 +500,7 @@ module ActionDispatch
routes.empty?
end
- def add_route(app, conditions = {}, requirements = {}, defaults = {}, name = nil, anchor = true)
+ def add_route(mapping, path_ast, name, anchor)
raise ArgumentError, "Invalid route name: '#{name}'" unless name.blank? || name.to_s.match(/^[_a-z]\w*$/i)
if name && named_routes[name]
@@ -509,74 +511,32 @@ module ActionDispatch
"http://guides.rubyonrails.org/routing.html#restricting-the-routes-created"
end
- path = conditions.delete :path_info
- ast = conditions.delete :parsed_path_info
- path = build_path(path, ast, requirements, anchor)
- conditions = build_conditions(conditions, path.names.map(&:to_sym))
-
- route = @set.add_route(app, path, conditions, defaults, name)
+ route = @set.add_route(name, mapping)
named_routes[name] = route if name
- route
- end
-
- def build_path(path, ast, requirements, anchor)
- strexp = Journey::Router::Strexp.new(
- ast,
- path,
- requirements,
- SEPARATORS,
- anchor)
-
- pattern = Journey::Path::Pattern.new(strexp)
-
- builder = Journey::GTG::Builder.new pattern.spec
-
- # Get all the symbol nodes followed by literals that are not the
- # dummy node.
- symbols = pattern.spec.grep(Journey::Nodes::Symbol).find_all { |n|
- builder.followpos(n).first.literal?
- }
-
- # Get all the symbol nodes preceded by literals.
- symbols.concat pattern.spec.find_all(&:literal?).map { |n|
- builder.followpos(n).first
- }.find_all(&:symbol?)
-
- symbols.each { |x|
- x.regexp = /(?:#{Regexp.union(x.regexp, '-')})+/
- }
- pattern
- end
- private :build_path
-
- def build_conditions(current_conditions, path_values)
- conditions = current_conditions.dup
-
- # Rack-Mount requires that :request_method be a regular expression.
- # :request_method represents the HTTP verb that matches this route.
- #
- # Here we munge values before they get sent on to rack-mount.
- verbs = conditions[:request_method] || []
- unless verbs.empty?
- conditions[:request_method] = %r[^#{verbs.join('|')}$]
+ if route.segment_keys.include?(:controller)
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Using a dynamic :controller segment in a route is deprecated and
+ will be removed in Rails 5.1.
+ MSG
end
- conditions.keep_if do |k, _|
- k == :action || k == :controller || k == :required_defaults ||
- request_class.public_method_defined?(k) || path_values.include?(k)
+ if route.segment_keys.include?(:action)
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Using a dynamic :action segment in a route is deprecated and
+ will be removed in Rails 5.1.
+ MSG
end
+
+ route
end
- private :build_conditions
class Generator
PARAMETERIZE = lambda do |name, value|
if name == :controller
value
- elsif value.is_a?(Array)
- value.map(&:to_param).join('/')
- elsif param = value.to_param
- param
+ else
+ value.to_param
end
end
@@ -584,16 +544,14 @@ module ActionDispatch
def initialize(named_route, options, recall, set)
@named_route = named_route
- @options = options.dup
- @recall = recall.dup
+ @options = options
+ @recall = recall
@set = set
- normalize_recall!
normalize_options!
normalize_controller_action_id!
use_relative_controller!
normalize_controller!
- normalize_action!
end
def controller
@@ -607,16 +565,11 @@ module ActionDispatch
def use_recall_for(key)
if @recall[key] && (!@options.key?(key) || @options[key] == @recall[key])
if !named_route_exists? || segment_keys.include?(key)
- @options[key] = @recall.delete(key)
+ @options[key] = @recall[key]
end
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
@@ -628,12 +581,12 @@ module ActionDispatch
# be "index", not the recalled action of "show".
if options[:controller]
- options[:action] ||= 'index'
+ options[:action] ||= "index"
options[:controller] = options[:controller].to_s
end
if options.key?(:action)
- options[:action] = (options[:action] || 'index').to_s
+ options[:action] = (options[:action] || "index").to_s
end
end
@@ -643,8 +596,8 @@ module ActionDispatch
# :controller, :action or :id is not found, don't pull any
# more keys from the recall.
def normalize_controller_action_id!
- use_recall_for(:controller) or return
- use_recall_for(:action) or return
+ use_recall_for(:controller) || return
+ use_recall_for(:action) || return
use_recall_for(:id)
end
@@ -652,7 +605,7 @@ module ActionDispatch
# is specified, the controller becomes "foo/baz/bat"
def use_relative_controller!
if !named_route && different_controller? && !controller.start_with?("/")
- old_parts = current_controller.split('/')
+ old_parts = current_controller.split("/")
size = controller.count("/") + 1
parts = old_parts[0...-size] << controller
@options[:controller] = parts.join("/")
@@ -661,13 +614,12 @@ module ActionDispatch
# Remove leading slashes from controllers
def normalize_controller!
- @options[:controller] = controller.sub(%r{^/}, '') if controller
- end
-
- # Move 'index' action from options to recall
- def normalize_action!
- if @options[:action] == 'index'
- @recall[:action] = @options.delete(:action)
+ if controller
+ if controller.start_with?("/".freeze)
+ @options[:controller] = controller[1..-1]
+ else
+ @options[:controller] = controller
+ end
end
end
@@ -694,11 +646,11 @@ module ActionDispatch
# 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={})
+ def extra_keys(options, recall = {})
generate_extras(options, recall).last
end
- def generate_extras(options, recall={})
+ def generate_extras(options, recall = {})
route_key = options.delete :use_route
path, params = generate(route_key, options, recall)
return path, params.keys
@@ -711,14 +663,18 @@ module ActionDispatch
RESERVED_OPTIONS = [:host, :protocol, :port, :subdomain, :domain, :tld_length,
:trailing_slash, :anchor, :params, :only_path, :script_name,
- :original_script_name]
+ :original_script_name, :relative_url_root]
def optimize_routes_generation?
default_url_options.empty?
end
def find_script_name(options)
- options.delete(:script_name) || relative_url_root || ''
+ options.delete(:script_name) || find_relative_url_root(options) || ""
+ end
+
+ def find_relative_url_root(options)
+ options.delete(:relative_url_root) || relative_url_root
end
def path_for(options, route_name = nil)
@@ -736,7 +692,7 @@ module ActionDispatch
password = options.delete :password
end
- recall = options.delete(:_recall) { {} }
+ recall = options.delete(:_recall) { {} }
original_script_name = options.delete(:original_script_name)
script_name = find_script_name options
@@ -764,7 +720,7 @@ module ActionDispatch
end
def call(env)
- req = request_class.new(env)
+ req = make_request(env)
req.path_info = Journey::Router::Utils.normalize_path(req.path_info)
@router.serve(req)
end
@@ -775,12 +731,12 @@ module ActionDispatch
extras = environment[:extras] || {}
begin
- env = Rack::MockRequest.env_for(path, {:method => method})
+ env = Rack::MockRequest.env_for(path, method: method)
rescue URI::InvalidURIError => e
raise ActionController::RoutingError, e.message
end
- req = request_class.new(env)
+ req = make_request(env)
@router.recognize(req) do |route, params|
params.merge!(extras)
params.each do |key, value|
@@ -793,14 +749,13 @@ module ActionDispatch
req.path_parameters = old_params.merge params
app = route.app
if app.matches?(req) && app.dispatcher?
- dispatcher = app.app
-
- if dispatcher.controller(params, false)
- dispatcher.prepare_params!(params)
- return params
- else
+ begin
+ req.controller_class
+ rescue NameError
raise ActionController::RoutingError, "A route matches #{path.inspect}, but references missing controller: #{params[:controller].camelize}Controller"
end
+
+ return req.path_parameters
end
end
diff --git a/actionpack/lib/action_dispatch/routing/routes_proxy.rb b/actionpack/lib/action_dispatch/routing/routes_proxy.rb
index 040ea04046..ee847eaeed 100644
--- a/actionpack/lib/action_dispatch/routing/routes_proxy.rb
+++ b/actionpack/lib/action_dispatch/routing/routes_proxy.rb
@@ -1,4 +1,4 @@
-require 'active_support/core_ext/array/extract_options'
+require "active_support/core_ext/array/extract_options"
module ActionDispatch
module Routing
@@ -19,7 +19,7 @@ module ActionDispatch
end
end
- def respond_to?(method, include_private = false)
+ def respond_to_missing?(method, include_private = false)
super || @helpers.respond_to?(method)
end
diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb
index eb554ec383..a1ac5a2b6c 100644
--- a/actionpack/lib/action_dispatch/routing/url_for.rb
+++ b/actionpack/lib/action_dispatch/routing/url_for.rb
@@ -1,7 +1,7 @@
module ActionDispatch
module Routing
# In <tt>config/routes.rb</tt> you define URL-to-controller mappings, but the reverse
- # is also possible: an URL can be generated from one of your routing definitions.
+ # is also possible: a URL can be generated from one of your routing definitions.
# URL generation functionality is centralized in this module.
#
# See ActionDispatch::Routing for general information about routing and routes.rb.
@@ -52,9 +52,11 @@ module ActionDispatch
# argument.
#
# For convenience reasons, mailers provide a shortcut for ActionController::UrlFor#url_for.
- # So within mailers, you only have to type 'url_for' instead of 'ActionController::UrlFor#url_for'
- # in full. However, mailers don't have hostname information, and that's why you'll still
- # have to specify the <tt>:host</tt> argument when generating URLs in mailers.
+ # So within mailers, you only have to type +url_for+ instead of 'ActionController::UrlFor#url_for'
+ # in full. However, mailers don't have hostname information, and you still have to provide
+ # the +:host+ argument or set the default host that will be used in all mailers using the
+ # configuration option +config.action_mailer.default_url_options+. For more information on
+ # url_for in mailers read the ActionMailer#Base documentation.
#
#
# == URL generation for named routes
@@ -147,6 +149,20 @@ module ActionDispatch
# # => 'http://somehost.org/myapp/tasks/testing'
# url_for controller: 'tasks', action: 'testing', host: 'somehost.org', script_name: "/myapp", only_path: true
# # => '/myapp/tasks/testing'
+ #
+ # Missing routes keys may be filled in from the current request's parameters
+ # (e.g. +:controller+, +:action+, +:id+ and any other parameters that are
+ # placed in the path). Given that the current action has been reached
+ # through `GET /users/1`:
+ #
+ # url_for(only_path: true) # => '/users/1'
+ # url_for(only_path: true, action: 'edit') # => '/users/1/edit'
+ # url_for(only_path: true, action: 'edit', id: 2) # => '/users/2/edit'
+ #
+ # Notice that no +:id+ parameter was provided to the first +url_for+ call
+ # and the helper used the one from the route's path. Any path parameter
+ # implicitly used by +url_for+ can always be overwritten like shown on the
+ # last +url_for+ calls.
def url_for(options = nil)
case options
when nil
@@ -155,12 +171,20 @@ module ActionDispatch
route_name = options.delete :use_route
_routes.url_for(options.symbolize_keys.reverse_merge!(url_options),
route_name)
+ when ActionController::Parameters
+ unless options.permitted?
+ raise ArgumentError.new(ActionDispatch::Routing::INSECURE_URL_PARAMETERS_MESSAGE)
+ end
+ route_name = options.delete :use_route
+ _routes.url_for(options.to_h.symbolize_keys.
+ reverse_merge!(url_options), route_name)
when String
options
when Symbol
HelperMethodBuilder.url.handle_string_call self, options
when Array
- polymorphic_url(options, options.extract_options!)
+ components = options.dup
+ polymorphic_url(components, components.extract_options!)
when Class
HelperMethodBuilder.url.handle_class_call self, options
else
@@ -170,20 +194,20 @@ module ActionDispatch
protected
- def optimize_routes_generation?
- _routes.optimize_routes_generation? && default_url_options.empty?
- end
+ def optimize_routes_generation?
+ _routes.optimize_routes_generation? && default_url_options.empty?
+ end
- def _with_routes(routes)
- old_routes, @_routes = @_routes, routes
- yield
- ensure
- @_routes = old_routes
- end
+ def _with_routes(routes)
+ old_routes, @_routes = @_routes, routes
+ yield
+ ensure
+ @_routes = old_routes
+ end
- def _routes_context
- self
- end
+ def _routes_context
+ self
+ end
end
end
end