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.rb121
-rw-r--r--actionpack/lib/action_dispatch/routing/mapper.rb432
-rw-r--r--actionpack/lib/action_dispatch/routing/polymorphic_routes.rb36
-rw-r--r--actionpack/lib/action_dispatch/routing/redirection.rb75
-rw-r--r--actionpack/lib/action_dispatch/routing/route_set.rb259
-rw-r--r--actionpack/lib/action_dispatch/routing/url_for.rb44
6 files changed, 634 insertions, 333 deletions
diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb
new file mode 100644
index 0000000000..8d7461ecc3
--- /dev/null
+++ b/actionpack/lib/action_dispatch/routing/inspector.rb
@@ -0,0 +1,121 @@
+require 'delegate'
+
+module ActionDispatch
+ module Routing
+ class RouteWrapper < SimpleDelegator
+ def endpoint
+ rack_app ? rack_app.inspect : "#{controller}##{action}"
+ end
+
+ def constraints
+ requirements.except(:controller, :action)
+ end
+
+ def rack_app(app = self.app)
+ @rack_app ||= begin
+ class_name = app.class.name.to_s
+ if class_name == "ActionDispatch::Routing::Mapper::Constraints"
+ rack_app(app.app)
+ elsif ActionDispatch::Routing::Redirect === app || class_name !~ /^ActionDispatch::Routing/
+ app
+ end
+ end
+ end
+
+ def verb
+ super.source.gsub(/[$^]/, '')
+ end
+
+ def path
+ super.spec.to_s
+ end
+
+ def name
+ super.to_s
+ end
+
+ def reqs
+ @reqs ||= begin
+ reqs = endpoint
+ reqs += " #{constraints.to_s}" unless constraints.empty?
+ reqs
+ end
+ end
+
+ def controller
+ requirements[:controller] || ':controller'
+ end
+
+ def action
+ requirements[:action] || ':action'
+ end
+
+ def internal?
+ controller =~ %r{\Arails/(info|welcome)} || path =~ %r{\A#{Rails.application.config.assets.prefix}}
+ end
+
+ def engine?
+ rack_app && rack_app.respond_to?(:routes)
+ end
+ end
+
+ ##
+ # This class is just used for displaying route information when someone
+ # executes `rake routes`. People should not use this class.
+ class RoutesInspector # :nodoc:
+ def initialize
+ @engines = Hash.new
+ end
+
+ def format(all_routes, filter = nil)
+ if filter
+ all_routes = all_routes.select{ |route| route.defaults[:controller] == filter }
+ end
+
+ routes = collect_routes(all_routes)
+
+ formatted_routes(routes) +
+ formatted_routes_for_engines
+ end
+
+ def collect_routes(routes)
+ routes = routes.collect do |route|
+ RouteWrapper.new(route)
+ end.reject do |route|
+ route.internal?
+ end.collect do |route|
+ collect_engine_routes(route)
+
+ {:name => route.name, :verb => route.verb, :path => route.path, :reqs => route.reqs }
+ end
+ end
+
+ 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)
+ end
+ end
+
+ def formatted_routes_for_engines
+ @engines.map do |name, routes|
+ ["\nRoutes for #{name}:"] + formatted_routes(routes)
+ end.flatten
+ end
+
+ def formatted_routes(routes)
+ name_width = routes.map{ |r| r[:name].length }.max
+ verb_width = routes.map{ |r| r[:verb].length }.max
+ path_width = routes.map{ |r| r[:path].length }.max
+
+ routes.map do |r|
+ "#{r[:name].rjust(name_width)} #{r[:verb].ljust(verb_width)} #{r[:path].ljust(path_width)} #{r[:reqs]}"
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb
index cd215034dc..3c99932e72 100644
--- a/actionpack/lib/action_dispatch/routing/mapper.rb
+++ b/actionpack/lib/action_dispatch/routing/mapper.rb
@@ -1,6 +1,7 @@
require 'active_support/core_ext/hash/except'
-require 'active_support/core_ext/object/blank'
-require 'active_support/core_ext/object/inclusion'
+require 'active_support/core_ext/hash/reverse_merge'
+require 'active_support/core_ext/hash/slice'
+require 'active_support/core_ext/enumerable'
require 'active_support/inflector'
require 'action_dispatch/routing/redirection'
@@ -34,6 +35,8 @@ module ActionDispatch
}
return true
+ ensure
+ req.reset_parameters
end
def call(env)
@@ -58,6 +61,16 @@ module ActionDispatch
@options = (@scope[:options] || {}).merge(options)
@path = normalize_path(path)
normalize_options!
+
+ via_all = @options.delete(:via) if @options[:via] == :all
+
+ if !via_all && request_method_condition.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 GET, use `get` in the router:\n\n" \
+ " Instead of: match \"controller#action\"\n" \
+ " Do: get \"controller#action\""
+ raise msg
+ end
end
def to_route
@@ -87,6 +100,10 @@ module ActionDispatch
raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}"
end
end
+
+ if @options[:constraints].is_a?(Hash)
+ (@options[:defaults] ||= {}).reverse_merge!(defaults_from_constraints(@options[:constraints]))
+ end
end
# match "account/overview"
@@ -104,7 +121,7 @@ module ActionDispatch
# Add a default constraint for :controller path segments that matches namespaced
# controllers with default routes like :controller/:action/:id(.:format), e.g:
# GET /admin/products/show/1
- # => { :controller => 'admin/products', :action => 'show', :id => '1' }
+ # => { controller: 'admin/products', action: 'show', id: '1' }
@options[:controller] ||= /.+?/
end
@@ -135,13 +152,13 @@ module ActionDispatch
end
def conditions
- { :path_info => @path }.merge(constraints).merge(request_method_condition)
+ { :path_info => @path }.merge!(constraints).merge!(request_method_condition)
end
def requirements
@requirements ||= (@options[:constraints].is_a?(Hash) ? @options[:constraints] : {}).tap do |requirements|
requirements.reverse_merge!(@scope[:constraints]) if @scope[:constraints]
- @options.each { |k, v| requirements[k] = v if v.is_a?(Regexp) }
+ @options.each { |k, v| requirements[k] ||= v if v.is_a?(Regexp) }
end
end
@@ -232,6 +249,11 @@ module ActionDispatch
def default_action
@options[:action] || @scope[:action]
end
+
+ def defaults_from_constraints(constraints)
+ url_keys = [:protocol, :subdomain, :domain, :host, :port]
+ constraints.select { |k, v| url_keys.include?(k) && (v.is_a?(String) || v.is_a?(Fixnum)) }
+ end
end
# Invokes Rack::Mount::Utils.normalize path and ensure that
@@ -239,18 +261,18 @@ module ActionDispatch
# for root cases, where the latter is the correct one.
def self.normalize_path(path)
path = Journey::Router::Utils.normalize_path(path)
- path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^/]+\)$}
+ path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^)]+\)$}
path
end
def self.normalize_name(name)
- normalize_path(name)[1..-1].gsub("/", "_")
+ normalize_path(name)[1..-1].tr("/", "_")
end
module Base
# You can specify what Rails should route "/" to with the root method:
#
- # root :to => 'pages#main'
+ # root to: 'pages#main'
#
# For options, see +match+, as +root+ uses it internally.
#
@@ -263,7 +285,7 @@ module ActionDispatch
# of most Rails applications, this is beneficial.
def root(options = {})
options = { :to => options } if options.is_a?(String)
- match '/', { :as => :root }.merge(options)
+ match '/', { :as => :root, :via => :get }.merge!(options)
end
# Matches a url pattern to one or more routes. Any symbols in a pattern
@@ -277,7 +299,7 @@ module ActionDispatch
# and +:action+ to the controller's action. A pattern can also map
# wildcard segments (globs) to params:
#
- # match 'songs/*category/:title' => 'songs#show'
+ # match 'songs/*category/:title', to: 'songs#show'
#
# # 'songs/rock/classic/stairway-to-heaven' sets
# # params[:category] = 'rock/classic'
@@ -287,16 +309,20 @@ module ActionDispatch
# +:controller+ should be set in options or hash shorthand. Examples:
#
# match 'photos/:id' => 'photos#show'
- # match 'photos/:id', :to => 'photos#show'
- # match 'photos/:id', :controller => 'photos', :action => 'show'
+ # match 'photos/:id', to: 'photos#show'
+ # match 'photos/:id', controller: 'photos', action: 'show'
#
# A pattern can also point to a +Rack+ endpoint i.e. anything that
# responds to +call+:
#
- # match 'photos/:id' => lambda {|hash| [200, {}, "Coming soon"] }
- # match 'photos/:id' => PhotoRackApp
+ # match 'photos/:id', to: lambda {|hash| [200, {}, "Coming soon"] }
+ # match 'photos/:id', to: PhotoRackApp
# # Yes, controller actions are just rack endpoints
- # match 'photos/:id' => PhotosController.action(:show)
+ # match 'photos/:id', to: PhotosController.action(:show)
+ #
+ # Because request various HTTP verbs with a single action has security
+ # implications, is recommendable use HttpHelpers[rdoc-ref:HttpHelpers]
+ # instead +match+
#
# === Options
#
@@ -314,7 +340,7 @@ module ActionDispatch
# [:module]
# The namespace for :controller.
#
- # match 'path' => 'c#a', :module => 'sekret', :controller => 'posts'
+ # match 'path', to: 'c#a', module: 'sekret', controller: 'posts'
# #=> Sekret::PostsController
#
# See <tt>Scoping#namespace</tt> for its scope equivalent.
@@ -325,16 +351,17 @@ module ActionDispatch
# [:via]
# Allowed HTTP verb(s) for route.
#
- # match 'path' => 'c#a', :via => :get
- # match 'path' => 'c#a', :via => [:get, :post]
+ # match 'path', to: 'c#a', via: :get
+ # match 'path', to: 'c#a', via: [:get, :post]
+ # match 'path', to: 'c#a', via: :all
#
# [:to]
# Points to a +Rack+ endpoint. Can be an object that responds to
# +call+ or a string representing a controller's action.
#
- # match 'path', :to => 'controller#action'
- # match 'path', :to => lambda { |env| [200, {}, "Success!"] }
- # match 'path', :to => RackApp
+ # match 'path', to: 'controller#action'
+ # match 'path', to: lambda { |env| [200, {}, "Success!"] }
+ # match 'path', to: RackApp
#
# [:on]
# Shorthand for wrapping routes in a specific RESTful context. Valid
@@ -342,14 +369,14 @@ module ActionDispatch
# <tt>resource(s)</tt> block. For example:
#
# resource :bar do
- # match 'foo' => 'c#a', :on => :member, :via => [:get, :post]
+ # match 'foo', to: 'c#a', on: :member, via: [:get, :post]
# end
#
# Is equivalent to:
#
# resource :bar do
# member do
- # match 'foo' => 'c#a', :via => [:get, :post]
+ # match 'foo', to: 'c#a', via: [:get, :post]
# end
# end
#
@@ -357,12 +384,12 @@ module ActionDispatch
# Constrains parameters with a hash of regular expressions or an
# object that responds to <tt>matches?</tt>
#
- # match 'path/:id', :constraints => { :id => /[A-Z]\d{5}/ }
+ # match 'path/:id', constraints: { id: /[A-Z]\d{5}/ }
#
# class Blacklist
# def matches?(request) request.remote_ip == '1.2.3.4' end
# end
- # match 'path' => 'c#a', :constraints => Blacklist.new
+ # match 'path', to: 'c#a', constraints: Blacklist.new
#
# See <tt>Scoping#constraints</tt> for more examples with its scope
# equivalent.
@@ -371,7 +398,7 @@ module ActionDispatch
# Sets defaults for parameters
#
# # Sets params[:format] to 'jpg' by default
- # match 'path' => 'c#a', :defaults => { :format => 'jpg' }
+ # match 'path', to: 'c#a', defaults: { format: 'jpg' }
#
# See <tt>Scoping#defaults</tt> for its scope equivalent.
#
@@ -380,13 +407,17 @@ module ActionDispatch
# false, the pattern matches any request prefixed with the given path.
#
# # Matches any request starting with 'path'
- # match 'path' => 'c#a', :anchor => false
+ # match 'path', to: 'c#a', anchor: false
+ #
+ # [:format]
+ # Allows you to specify the default value for optional +format+
+ # segment or disable it by supplying +false+.
def match(path, options=nil)
end
# Mount a Rack-based application to be used within the application.
#
- # mount SomeRackApp, :at => "some_route"
+ # mount SomeRackApp, at: "some_route"
#
# Alternatively:
#
@@ -399,7 +430,7 @@ module ActionDispatch
# the helper is either +some_rack_app_path+ or +some_rack_app_url+.
# To customize this helper's name, use the +:as+ option:
#
- # mount(SomeRackApp => "some_route", :as => "exciting")
+ # mount(SomeRackApp => "some_route", as: "exciting")
#
# This will generate the +exciting_path+ and +exciting_url+ helpers
# which can be used to navigate to this mounted app.
@@ -407,6 +438,10 @@ module ActionDispatch
if options
path = options.delete(:at)
else
+ unless Hash === app
+ raise ArgumentError, "must be called with mount point"
+ end
+
options = app
app, path = options.find { |k, v| k.respond_to?(:call) }
options.delete(app) if app
@@ -414,7 +449,8 @@ module ActionDispatch
raise "A rack application must be specified" unless path
- options[:as] ||= app_name(app)
+ options[:as] ||= app_name(app)
+ options[:via] ||= :all
match(path, options.merge(:to => app, :anchor => false, :format => false))
@@ -441,7 +477,7 @@ module ActionDispatch
app.railtie_name
else
class_name = app.class.is_a?(Class) ? app.name : app.class.name
- ActiveSupport::Inflector.underscore(class_name).gsub("/", "_")
+ ActiveSupport::Inflector.underscore(class_name).tr("/", "_")
end
end
@@ -460,9 +496,7 @@ module ActionDispatch
prefix_options = options.slice(*_route.segment_keys)
# we must actually delete prefix segment keys to avoid passing them to next url_for
_route.segment_keys.each { |k| options.delete(k) }
- prefix = _routes.url_helpers.send("#{name}_path", prefix_options)
- prefix = '' if prefix == '/'
- prefix
+ _routes.url_helpers.send("#{name}_path", prefix_options)
end
end
end
@@ -470,51 +504,41 @@ module ActionDispatch
module HttpHelpers
# Define a route that only recognizes HTTP GET.
- # For supported arguments, see <tt>Base#match</tt>.
+ # For supported arguments, see match[rdoc-ref:Base#match]
#
- # Example:
- #
- # get 'bacon', :to => 'food#bacon'
+ # get 'bacon', to: 'food#bacon'
def get(*args, &block)
map_method(:get, args, &block)
end
# Define a route that only recognizes HTTP POST.
- # For supported arguments, see <tt>Base#match</tt>.
- #
- # Example:
+ # For supported arguments, see match[rdoc-ref:Base#match]
#
- # post 'bacon', :to => 'food#bacon'
+ # post 'bacon', to: 'food#bacon'
def post(*args, &block)
map_method(:post, args, &block)
end
# Define a route that only recognizes HTTP PATCH.
- # For supported arguments, see <tt>Base#match</tt>.
- #
- # Example:
+ # For supported arguments, see match[rdoc-ref:Base#match]
#
- # patch 'bacon', :to => 'food#bacon'
+ # patch 'bacon', to: 'food#bacon'
def patch(*args, &block)
map_method(:patch, args, &block)
end
# Define a route that only recognizes HTTP PUT.
- # For supported arguments, see <tt>Base#match</tt>.
+ # For supported arguments, see match[rdoc-ref:Base#match]
#
- # Example:
- #
- # put 'bacon', :to => 'food#bacon'
+ # put 'bacon', to: 'food#bacon'
def put(*args, &block)
map_method(:put, args, &block)
end
# Define a route that only recognizes HTTP DELETE.
- # For supported arguments, see <tt>Base#match</tt>.
- #
- # Example:
+ # For supported arguments, see match[rdoc-ref:Base#match]
#
- # delete 'broccoli', :to => 'food#broccoli'
+ # delete 'broccoli', to: 'food#broccoli'
def delete(*args, &block)
map_method(:delete, args, &block)
end
@@ -522,7 +546,8 @@ module ActionDispatch
private
def map_method(method, args, &block)
options = args.extract_options!
- options[:via] = method
+ options[:via] = method
+ options[:path] ||= args.first if args.first.is_a?(String)
match(*args, options, &block)
self
end
@@ -552,13 +577,13 @@ module ActionDispatch
# If you want to route /posts (without the prefix /admin) to
# <tt>Admin::PostsController</tt>, you could use
#
- # scope :module => "admin" do
+ # scope module: "admin" do
# resources :posts
# end
#
# or, for a single case
#
- # resources :posts, :module => "admin"
+ # resources :posts, module: "admin"
#
# If you want to route /admin/posts to +PostsController+
# (without the Admin:: module prefix), you could use
@@ -569,7 +594,7 @@ module ActionDispatch
#
# or, for a single case
#
- # resources :posts, :path => "/admin/posts"
+ # resources :posts, path: "/admin/posts"
#
# In each of these cases, the named routes remain the same as if you did
# not use scope. In the last case, the following paths map to
@@ -587,7 +612,7 @@ module ActionDispatch
#
# Take the following route definition as an example:
#
- # scope :path => ":account_id", :as => "account" do
+ # scope path: ":account_id", as: "account" do
# resources :projects
# end
#
@@ -599,31 +624,30 @@ module ActionDispatch
#
# Takes same options as <tt>Base#match</tt> and <tt>Resources#resources</tt>.
#
- # === Examples
- #
# # route /posts (without the prefix /admin) to <tt>Admin::PostsController</tt>
- # scope :module => "admin" do
+ # scope module: "admin" do
# resources :posts
# end
#
# # prefix the posts resource's requests with '/admin'
- # scope :path => "/admin" do
+ # scope path: "/admin" do
# resources :posts
# end
#
# # prefix the routing helper name: +sekret_posts_path+ instead of +posts_path+
- # scope :as => "sekret" do
+ # scope as: "sekret" do
# resources :posts
# end
def scope(*args)
- options = args.extract_options!
- options = options.dup
-
- options[:path] = args.first if args.first.is_a?(String)
+ options = args.extract_options!.dup
recover = {}
+ options[:path] = args.flatten.join('/') if args.any?
options[:constraints] ||= {}
- unless options[:constraints].is_a?(Hash)
+
+ if options[:constraints].is_a?(Hash)
+ (options[:defaults] ||= {}).reverse_merge!(defaults_from_constraints(options[:constraints]))
+ else
block, options[:constraints] = options[:constraints], {}
end
@@ -634,8 +658,8 @@ module ActionDispatch
end
end
- recover[:block] = @scope[:blocks]
- @scope[:blocks] = merge_blocks_scope(@scope[:blocks], block)
+ recover[:blocks] = @scope[:blocks]
+ @scope[:blocks] = merge_blocks_scope(@scope[:blocks], block)
recover[:options] = @scope[:options]
@scope[:options] = merge_options_scope(@scope[:options], options)
@@ -643,19 +667,13 @@ module ActionDispatch
yield
self
ensure
- scope_options.each do |option|
- @scope[option] = recover[option] if recover.has_key?(option)
- end
-
- @scope[:options] = recover[:options]
- @scope[:blocks] = recover[:block]
+ @scope.merge!(recover)
end
# Scopes routes to a specific controller
#
- # Example:
# controller "food" do
- # match "bacon", :action => "bacon"
+ # match "bacon", action: "bacon"
# end
def controller(controller, options={})
options[:controller] = controller
@@ -686,20 +704,18 @@ module ActionDispatch
# For options, see <tt>Base#match</tt>. For +:shallow_path+ option, see
# <tt>Resources#resources</tt>.
#
- # === Examples
- #
# # accessible through /sekret/posts rather than /admin/posts
- # namespace :admin, :path => "sekret" do
+ # namespace :admin, path: "sekret" do
# resources :posts
# end
#
# # maps to <tt>Sekret::PostsController</tt> rather than <tt>Admin::PostsController</tt>
- # namespace :admin, :module => "sekret" do
+ # namespace :admin, module: "sekret" do
# resources :posts
# end
#
# # generates +sekret_posts_path+ rather than +admin_posts_path+
- # namespace :admin, :as => "sekret" do
+ # namespace :admin, as: "sekret" do
# resources :posts
# end
def namespace(path, options = {})
@@ -713,7 +729,7 @@ module ActionDispatch
# Allows you to constrain the nested routes based on a set of rules.
# For instance, in order to change the routes to allow for a dot character in the +id+ parameter:
#
- # constraints(:id => /\d+\.\d+/) do
+ # constraints(id: /\d+\.\d+/) do
# resources :posts
# end
#
@@ -723,7 +739,7 @@ module ActionDispatch
# You may use this to also restrict other parameters:
#
# resources :posts do
- # constraints(:post_id => /\d+\.\d+/) do
+ # constraints(post_id: /\d+\.\d+/) do
# resources :comments
# end
# end
@@ -732,7 +748,7 @@ module ActionDispatch
#
# Routes can also be constrained to an IP or a certain range of IP addresses:
#
- # constraints(:ip => /192.168.\d+.\d+/) do
+ # constraints(ip: /192\.168\.\d+\.\d+/) do
# resources :posts
# end
#
@@ -769,8 +785,8 @@ module ActionDispatch
end
# Allows you to set default parameters for a route, such as this:
- # defaults :id => 'home' do
- # match 'scoped_pages/(:id)', :to => 'pages#show'
+ # defaults id: 'home' do
+ # match 'scoped_pages/(:id)', to: 'pages#show'
# end
# Using this, the +:id+ parameter here will default to 'home'.
def defaults(defaults = {})
@@ -825,7 +841,7 @@ module ActionDispatch
end
def merge_options_scope(parent, child) #:nodoc:
- (parent || {}).except(*override_keys(child)).merge(child)
+ (parent || {}).except(*override_keys(child)).merge!(child)
end
def merge_shallow_scope(parent, child) #:nodoc:
@@ -835,6 +851,11 @@ module ActionDispatch
def override_keys(child) #:nodoc:
child.key?(:only) || child.key?(:except) ? [:only, :except] : []
end
+
+ def defaults_from_constraints(constraints)
+ url_keys = [:protocol, :subdomain, :domain, :host, :port]
+ constraints.select { |k, v| url_keys.include?(k) && (v.is_a?(String) || v.is_a?(Fixnum)) }
+ end
end
# Resource routing allows you to quickly declare all of the common routes
@@ -872,7 +893,7 @@ module ActionDispatch
# use dots as part of the +:id+ parameter add a constraint which
# overrides this restriction, e.g:
#
- # resources :articles, :id => /[^\/]+/
+ # resources :articles, id: /[^\/]+/
#
# This allows any character other than a slash as part of your +:id+.
#
@@ -880,17 +901,18 @@ module ActionDispatch
# CANONICAL_ACTIONS holds all actions that does not need a prefix or
# a path appended since they fit properly in their scope level.
VALID_ON_OPTIONS = [:new, :collection, :member]
- RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except]
+ RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except, :param, :concerns]
CANONICAL_ACTIONS = %w(index create new show update destroy)
class Resource #:nodoc:
- attr_reader :controller, :path, :options
+ attr_reader :controller, :path, :options, :param
def initialize(entities, 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
end
@@ -935,15 +957,21 @@ module ActionDispatch
alias :collection_scope :path
def member_scope
- "#{path}/:id"
+ "#{path}/:#{param}"
end
+ alias :shallow_scope :member_scope
+
def new_scope(new_path)
"#{path}/#{new_path}"
end
+ def nested_param
+ :"#{singular}_#{param}"
+ end
+
def nested_scope
- "#{path}/:#{singular}_id"
+ "#{path}/:#{nested_param}"
end
end
@@ -1001,7 +1029,7 @@ module ActionDispatch
# === Options
# Takes same options as +resources+.
def resource(*resources, &block)
- options = resources.extract_options!
+ options = resources.extract_options!.dup
if apply_common_behavior_for(:resource, resources, options, &block)
return self
@@ -1010,6 +1038,8 @@ module ActionDispatch
resource_scope(:resource, SingletonResource.new(resources.pop, options)) do
yield if block_given?
+ concerns(options[:concerns]) if options[:concerns]
+
collection do
post :create
end if parent_resource.actions.include?(:create)
@@ -1018,15 +1048,7 @@ module ActionDispatch
get :new
end if parent_resource.actions.include?(:new)
- member do
- get :edit if parent_resource.actions.include?(:edit)
- get :show if parent_resource.actions.include?(:show)
- if parent_resource.actions.include?(:update)
- patch :update
- put :update
- end
- delete :destroy if parent_resource.actions.include?(:destroy)
- end
+ set_member_mappings_for_resource
end
self
@@ -1073,43 +1095,43 @@ module ActionDispatch
# Allows you to change the segment component of the +edit+ and +new+ actions.
# Actions not specified are not changed.
#
- # resources :posts, :path_names => { :new => "brand_new" }
+ # resources :posts, path_names: { new: "brand_new" }
#
# The above example will now change /posts/new to /posts/brand_new
#
# [:path]
# Allows you to change the path prefix for the resource.
#
- # resources :posts, :path => 'postings'
+ # resources :posts, path: 'postings'
#
# The resource and all segments will now route to /postings instead of /posts
#
# [:only]
# Only generate routes for the given actions.
#
- # resources :cows, :only => :show
- # resources :cows, :only => [:show, :index]
+ # resources :cows, only: :show
+ # resources :cows, only: [:show, :index]
#
# [:except]
# Generate all routes except for the given actions.
#
- # resources :cows, :except => :show
- # resources :cows, :except => [:show, :index]
+ # resources :cows, except: :show
+ # resources :cows, except: [:show, :index]
#
# [:shallow]
# Generates shallow routes for nested resource(s). When placed on a parent resource,
# generates shallow routes for all nested resources.
#
- # resources :posts, :shallow => true do
+ # resources :posts, shallow: true do
# resources :comments
# end
#
# Is the same as:
#
# resources :posts do
- # resources :comments, :except => [:show, :edit, :update, :destroy]
+ # resources :comments, except: [:show, :edit, :update, :destroy]
# end
- # resources :comments, :only => [:show, :edit, :update, :destroy]
+ # resources :comments, only: [:show, :edit, :update, :destroy]
#
# This allows URLs for resources that otherwise would be deeply nested such
# as a comment on a blog post like <tt>/posts/a-long-permalink/comments/1234</tt>
@@ -1118,9 +1140,9 @@ module ActionDispatch
# [:shallow_path]
# Prefixes nested shallow routes with the specified path.
#
- # scope :shallow_path => "sekret" do
+ # scope shallow_path: "sekret" do
# resources :posts do
- # resources :comments, :shallow => true
+ # resources :comments, shallow: true
# end
# end
#
@@ -1134,15 +1156,38 @@ module ActionDispatch
# comment PATCH/PUT /sekret/comments/:id(.:format)
# comment DELETE /sekret/comments/:id(.:format)
#
+ # [:shallow_prefix]
+ # Prefixes nested shallow route names with specified prefix.
+ #
+ # scope shallow_prefix: "sekret" do
+ # resources :posts do
+ # resources :comments, shallow: true
+ # end
+ # end
+ #
+ # The +comments+ resource here will have the following routes generated for it:
+ #
+ # post_comments GET /posts/:post_id/comments(.:format)
+ # post_comments POST /posts/:post_id/comments(.:format)
+ # new_post_comment GET /posts/:post_id/comments/new(.:format)
+ # edit_sekret_comment GET /comments/:id/edit(.:format)
+ # sekret_comment GET /comments/:id(.:format)
+ # sekret_comment PATCH/PUT /comments/:id(.:format)
+ # sekret_comment DELETE /comments/:id(.:format)
+ #
+ # [:format]
+ # Allows you to specify the default value for optional +format+
+ # segment or disable it by supplying +false+.
+ #
# === Examples
#
# # routes call <tt>Admin::PostsController</tt>
- # resources :posts, :module => "admin"
+ # resources :posts, module: "admin"
#
# # resource actions are at /admin/posts.
- # resources :posts, :path => "admin/posts"
+ # resources :posts, path: "admin/posts"
def resources(*resources, &block)
- options = resources.extract_options!
+ options = resources.extract_options!.dup
if apply_common_behavior_for(:resources, resources, options, &block)
return self
@@ -1151,6 +1196,8 @@ module ActionDispatch
resource_scope(:resources, Resource.new(resources.pop, options)) do
yield if block_given?
+ concerns(options[:concerns]) if options[:concerns]
+
collection do
get :index if parent_resource.actions.include?(:index)
post :create if parent_resource.actions.include?(:create)
@@ -1160,15 +1207,7 @@ module ActionDispatch
get :new
end if parent_resource.actions.include?(:new)
- member do
- get :edit if parent_resource.actions.include?(:edit)
- get :show if parent_resource.actions.include?(:show)
- if parent_resource.actions.include?(:update)
- patch :update
- put :update
- end
- delete :destroy if parent_resource.actions.include?(:destroy)
- end
+ set_member_mappings_for_resource
end
self
@@ -1329,7 +1368,7 @@ module ActionDispatch
options[:as] = name_for_action(options[:as], action)
end
- mapping = Mapping.new(@set, @scope, path, options)
+ mapping = Mapping.new(@set, @scope, URI.parser.escape(path), options)
app, conditions, requirements, defaults, as, anchor = mapping.to_route
@set.add_route(app, conditions, requirements, defaults, as, anchor)
end
@@ -1435,18 +1474,18 @@ module ActionDispatch
def nested_options #:nodoc:
options = { :as => parent_resource.member_name }
options[:constraints] = {
- :"#{parent_resource.singular}_id" => id_constraint
- } if id_constraint?
+ parent_resource.nested_param => param_constraint
+ } if param_constraint?
options
end
- def id_constraint? #:nodoc:
- @scope[:constraints] && @scope[:constraints][:id].is_a?(Regexp)
+ def param_constraint? #:nodoc:
+ @scope[:constraints] && @scope[:constraints][parent_resource.param].is_a?(Regexp)
end
- def id_constraint #:nodoc:
- @scope[:constraints][:id]
+ def param_constraint #:nodoc:
+ @scope[:constraints][parent_resource.param]
end
def canonical_action?(action, flag) #:nodoc:
@@ -1459,9 +1498,9 @@ module ActionDispatch
def path_for_action(action, path) #:nodoc:
prefix = shallow_scoping? ?
- "#{@scope[:shallow_path]}/#{parent_resource.path}/:id" : @scope[:path]
+ "#{@scope[:shallow_path]}/#{parent_resource.shallow_scope}" : @scope[:path]
- path = if canonical_action?(action, path.blank?)
+ if canonical_action?(action, path.blank?)
prefix.to_s
else
"#{prefix}/#{action_path(action, path)}"
@@ -1519,17 +1558,136 @@ module ActionDispatch
end
end
end
+
+ def set_member_mappings_for_resource
+ member do
+ get :edit if parent_resource.actions.include?(:edit)
+ get :show if parent_resource.actions.include?(:show)
+ if parent_resource.actions.include?(:update)
+ patch :update
+ put :update
+ end
+ delete :destroy if parent_resource.actions.include?(:destroy)
+ end
+ end
+ end
+
+ # Routing Concerns allow you to declare common routes that can be reused
+ # inside others resources and routes.
+ #
+ # concern :commentable do
+ # resources :comments
+ # end
+ #
+ # concern :image_attachable do
+ # resources :images, only: :index
+ # end
+ #
+ # These concerns are used in Resources routing:
+ #
+ # resources :messages, concerns: [:commentable, :image_attachable]
+ #
+ # or in a scope or namespace:
+ #
+ # namespace :posts do
+ # concerns :commentable
+ # end
+ module Concerns
+ # Define a routing concern using a name.
+ #
+ # Concerns may be defined inline, using a block, or handled by
+ # another object, by passing that object as the second parameter.
+ #
+ # The concern object, if supplied, should respond to <tt>call</tt>,
+ # which will receive two parameters:
+ #
+ # * The current mapper
+ # * A hash of options which the concern object may use
+ #
+ # Options may also be used by concerns defined in a block by accepting
+ # a block parameter. So, using a block, you might do something as
+ # simple as limit the actions available on certain resources, passing
+ # standard resource options through the concern:
+ #
+ # concern :commentable do |options|
+ # resources :comments, options
+ # end
+ #
+ # resources :posts, concerns: :commentable
+ # resources :archived_posts do
+ # # Don't allow comments on archived posts
+ # concerns :commentable, only: [:index, :show]
+ # end
+ #
+ # Or, using a callable object, you might implement something more
+ # specific to your application, which would be out of place in your
+ # routes file.
+ #
+ # # purchasable.rb
+ # class Purchasable
+ # def initialize(defaults = {})
+ # @defaults = defaults
+ # end
+ #
+ # def call(mapper, options = {})
+ # options = @defaults.merge(options)
+ # mapper.resources :purchases
+ # mapper.resources :receipts
+ # mapper.resources :returns if options[:returnable]
+ # end
+ # end
+ #
+ # # routes.rb
+ # concern :purchasable, Purchasable.new(returnable: true)
+ #
+ # resources :toys, concerns: :purchasable
+ # resources :electronics, concerns: :purchasable
+ # resources :pets do
+ # concerns :purchasable, returnable: false
+ # end
+ #
+ # Any routing helpers can be used inside a concern. If using a
+ # callable, they're accessible from the Mapper that's passed to
+ # <tt>call</tt>.
+ def concern(name, callable = nil, &block)
+ callable ||= lambda { |mapper, options| mapper.instance_exec(options, &block) }
+ @concerns[name] = callable
+ end
+
+ # Use the named concerns
+ #
+ # resources :posts do
+ # concerns :commentable
+ # end
+ #
+ # concerns also work in any routes helper that you want to use:
+ #
+ # namespace :posts do
+ # concerns :commentable
+ # end
+ def concerns(*args)
+ options = args.extract_options!
+ args.flatten.each do |name|
+ if concern = @concerns[name]
+ concern.call(self, options)
+ else
+ raise ArgumentError, "No concern named #{name} was found!"
+ end
+ end
+ end
end
def initialize(set) #:nodoc:
@set = set
@scope = { :path_names => @set.resources_path_names }
+ @concerns = {}
end
include Base
include HttpHelpers
include Redirection
include Scoping
+ include Concerns
include Resources
end
end
diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
index 013cf93dbc..6d3f8da932 100644
--- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
+++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
@@ -1,3 +1,5 @@
+require 'action_controller/model_naming'
+
module ActionDispatch
module Routing
# Polymorphic URL helpers are methods for smart resolution to a named route call when
@@ -32,7 +34,7 @@ module ActionDispatch
# == Prefixed polymorphic helpers
#
# In addition to <tt>polymorphic_url</tt> and <tt>polymorphic_path</tt> methods, a
- # number of prefixed helpers are available as a shorthand to <tt>:action => "..."</tt>
+ # number of prefixed helpers are available as a shorthand to <tt>action: "..."</tt>
# in options. Those are:
#
# * <tt>edit_polymorphic_url</tt>, <tt>edit_polymorphic_path</tt>
@@ -41,20 +43,20 @@ module ActionDispatch
# Example usage:
#
# edit_polymorphic_path(@post) # => "/posts/1/edit"
- # polymorphic_path(@post, :format => :pdf) # => "/posts/1.pdf"
- #
- # == Using with mounted engines
+ # polymorphic_path(@post, format: :pdf) # => "/posts/1.pdf"
#
- # If you use mounted engine, there is a possibility that you will need to use
- # polymorphic_url pointing at engine's routes. To do that, just pass proxy used
- # to reach engine's routes as a first argument:
+ # == Usage with mounted engines
#
- # For example:
+ # If you are using a mounted engine and you need to use a polymorphic_url
+ # pointing at the engine's routes, pass in the engine's route proxy as the first
+ # argument to the method. For example:
#
- # polymorphic_url([blog, @post]) # it will call blog.post_path(@post)
- # form_for([blog, @post]) # => "/blog/posts/1
+ # polymorphic_url([blog, @post]) # calls blog.post_path(@post)
+ # form_for([blog, @post]) # => "/blog/posts/1"
#
module PolymorphicRoutes
+ include ActionController::ModelNaming
+
# Constructs a call to a named RESTful route for the given record and returns the
# resulting URL string. For example:
#
@@ -72,8 +74,6 @@ module ActionDispatch
# * <tt>:routing_type</tt> - Allowed values are <tt>:path</tt> or <tt>:url</tt>.
# Default is <tt>:url</tt>.
#
- # ==== Examples
- #
# # an Article record
# polymorphic_url(record) # same as article_url(record)
#
@@ -97,7 +97,7 @@ module ActionDispatch
end
record = extract_record(record_or_hash_or_array)
- record = record.to_model if record.respond_to?(:to_model)
+ record = convert_to_model(record)
args = Array === record_or_hash_or_array ?
record_or_hash_or_array.dup :
@@ -124,11 +124,13 @@ module ActionDispatch
args.last.kind_of?(Hash) ? args.last.merge!(url_options) : args << url_options
end
+ args.collect! { |a| convert_to_model(a) }
+
(proxy || self).send(named_route, *args)
end
# Returns the path component of a URL for the given record. It uses
- # <tt>polymorphic_url</tt> with <tt>:routing_type => :path</tt>.
+ # <tt>polymorphic_url</tt> with <tt>routing_type: :path</tt>.
def polymorphic_path(record_or_hash_or_array, options = {})
polymorphic_url(record_or_hash_or_array, options.merge(:routing_type => :path))
end
@@ -165,7 +167,7 @@ module ActionDispatch
if parent.is_a?(Symbol) || parent.is_a?(String)
parent
else
- ActiveModel::Naming.singular_route_key(parent)
+ model_name_from_record_or_class(parent).singular_route_key
end
end
else
@@ -177,9 +179,9 @@ module ActionDispatch
route << record
elsif record
if inflection == :singular
- route << ActiveModel::Naming.singular_route_key(record)
+ route << model_name_from_record_or_class(record).singular_route_key
else
- route << ActiveModel::Naming.route_key(record)
+ route << model_name_from_record_or_class(record).route_key
end
else
raise ArgumentError, "Nil location provided. Can't build URI."
diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb
index 617b24b46a..d751e04e6a 100644
--- a/actionpack/lib/action_dispatch/routing/redirection.rb
+++ b/actionpack/lib/action_dispatch/routing/redirection.rb
@@ -1,4 +1,8 @@
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'
module ActionDispatch
module Routing
@@ -13,6 +17,14 @@ module ActionDispatch
def call(env)
req = Request.new(env)
+ # If any of the path parameters has a invalid encoding then
+ # raise since it's likely to trigger errors further on.
+ req.symbolized_path_parameters.each do |key, value|
+ unless value.valid_encoding?
+ raise ActionController::BadRequest, "Invalid parameter: #{key} => #{value}"
+ end
+ end
+
uri = URI.parse(path(req.symbolized_path_parameters, req))
uri.scheme ||= req.scheme
uri.host ||= req.host
@@ -32,6 +44,25 @@ module ActionDispatch
def path(params, request)
block.call params, request
end
+
+ def inspect
+ "redirect(#{status})"
+ end
+ end
+
+ class PathRedirect < Redirect
+ def path(params, request)
+ (params.empty? || !block.match(/%\{\w*\}/)) ? block : (block % escape(params))
+ end
+
+ def inspect
+ "redirect(#{status}, #{block})"
+ end
+
+ private
+ def escape(params)
+ Hash[params.map{ |k,v| [k, Rack::Utils.escape(v)] }]
+ end
end
class OptionRedirect < Redirect # :nodoc:
@@ -44,21 +75,34 @@ module ActionDispatch
:port => request.optional_port,
:path => request.path,
:params => request.query_parameters
- }.merge options
+ }.merge! options
+
+ if !params.empty? && url_options[:path].match(/%\{\w*\}/)
+ url_options[:path] = (url_options[:path] % escape_path(params))
+ end
ActionDispatch::Http::URL.url_for url_options
end
+
+ def inspect
+ "redirect(#{status}, #{options.map{ |k,v| "#{k}: #{v}" }.join(', ')})"
+ end
+
+ private
+ def escape_path(params)
+ Hash[params.map{ |k,v| [k, URI.parser.escape(v)] }]
+ end
end
module Redirection
# Redirect any path to another path:
#
- # match "/stories" => redirect("/posts")
+ # get "/stories" => redirect("/posts")
#
# You can also use interpolation in the supplied redirect argument:
#
- # match 'docs/:article', :to => redirect('/wiki/%{article}')
+ # get 'docs/:article', to: redirect('/wiki/%{article}')
#
# Alternatively you can use one of the other syntaxes:
#
@@ -67,34 +111,33 @@ module ActionDispatch
# params, depending of how many arguments your block accepts. A string is required as a
# return value.
#
- # match 'jokes/:number', :to => redirect do |params, request|
- # path = (params[:number].to_i.even? ? "/wheres-the-beef" : "/i-love-lamp")
+ # get 'jokes/:number', to: redirect { |params, request|
+ # path = (params[:number].to_i.even? ? "wheres-the-beef" : "i-love-lamp")
# "http://#{request.host_with_port}/#{path}"
- # end
+ # }
+ #
+ # Note that the +do end+ syntax for the redirect block wouldn't work, as Ruby would pass
+ # the block to +get+ instead of +redirect+. Use <tt>{ ... }</tt> instead.
#
# The options version of redirect allows you to supply only the parts of the url which need
# to change, it also supports interpolation of the path similar to the first example.
#
- # match 'stores/:name', :to => redirect(:subdomain => 'stores', :path => '/%{name}')
- # match 'stores/:name(*all)', :to => redirect(:subdomain => 'stores', :path => '/%{name}%{all}')
+ # get 'stores/:name', to: redirect(subdomain: 'stores', path: '/%{name}')
+ # get 'stores/:name(*all)', to: redirect(subdomain: 'stores', path: '/%{name}%{all}')
#
# Finally, an object which responds to call can be supplied to redirect, allowing you to reuse
# common redirect routes. The call method must accept two arguments, params and request, and return
# a string.
#
- # match 'accounts/:name' => redirect(SubdomainRedirector.new('api'))
+ # get 'accounts/:name' => redirect(SubdomainRedirector.new('api'))
#
def redirect(*args, &block)
- options = args.last.is_a?(Hash) ? args.pop : {}
+ options = args.extract_options!
status = options.delete(:status) || 301
+ path = args.shift
return OptionRedirect.new(status, options) if options.any?
-
- path = args.shift
-
- block = lambda { |params, request|
- (params.empty? || !path.match(/%\{\w*\}/)) ? path : (path % params)
- } if String === path
+ return PathRedirect.new(status, path) if String === path
block = path if path.respond_to? :call
raise ArgumentError, "redirection argument not supported" unless block
diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb
index 30e9e5634b..b1959e388c 100644
--- a/actionpack/lib/action_dispatch/routing/route_set.rb
+++ b/actionpack/lib/action_dispatch/routing/route_set.rb
@@ -1,6 +1,6 @@
-require 'journey'
+require 'action_dispatch/journey'
require 'forwardable'
-require 'active_support/core_ext/object/blank'
+require 'thread_safe'
require 'active_support/core_ext/object/to_query'
require 'active_support/core_ext/hash/slice'
require 'active_support/core_ext/module/remove_method'
@@ -21,11 +21,20 @@ module ActionDispatch
def initialize(options={})
@defaults = options[:defaults]
@glob_param = options.delete(:glob)
- @controllers = {}
+ @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
+ # raise since it's likely to trigger errors further on.
+ params.each do |key, value|
+ unless value.valid_encoding?
+ raise ActionController::BadRequest, "Invalid parameter: #{key} => #{value}"
+ end
+ end
+
prepare_params!(params)
# Just raise undefined constant errors if a controller was specified as default.
@@ -60,13 +69,8 @@ module ActionDispatch
private
def controller_reference(controller_param)
- controller_name = "#{controller_param.camelize}Controller"
-
- unless controller = @controllers[controller_param]
- controller = @controllers[controller_param] =
- ActiveSupport::Dependencies.reference(controller_name)
- end
- controller.get(controller_name)
+ const_name = @controller_class_names[controller_param] ||= "#{controller_param.camelize}Controller"
+ ActiveSupport::Dependencies.constantize(const_name)
end
def dispatch(controller, action, env)
@@ -96,7 +100,25 @@ module ActionDispatch
def initialize
@routes = {}
@helpers = []
- @module = Module.new
+ @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
end
def helper_names
@@ -104,6 +126,12 @@ module ActionDispatch
end
def clear!
+ @helpers.each do |helper|
+ @module.module_eval do
+ remove_possible_method helper
+ end
+ end
+
@routes.clear
@helpers.clear
end
@@ -135,43 +163,12 @@ module ActionDispatch
end
private
- def url_helper_name(name, kind = :url)
- :"#{name}_#{kind}"
- end
-
- def hash_access_name(name, kind = :url)
- :"hash_for_#{name}_#{kind}"
- end
def define_named_route_methods(name, route)
- {:url => {:only_path => false}, :path => {:only_path => true}}.each do |kind, opts|
- hash = route.defaults.merge(:use_route => name).merge(opts)
- define_hash_access route, name, kind, hash
- define_url_helper route, name, kind, hash
- end
- end
-
- def define_hash_access(route, name, kind, options)
- selector = hash_access_name(name, kind)
-
- @module.module_eval do
- remove_possible_method selector
-
- define_method(selector) do |*args|
- inner_options = args.extract_options!
- result = options.dup
-
- if args.any?
- result[:_positional_args] = args
- result[:_positional_keys] = route.segment_keys
- end
-
- result.merge(inner_options)
- end
-
- protected selector
- end
- helpers << selector
+ 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
# Create a url helper allowing ordered parameters to be associated
@@ -181,44 +178,33 @@ module ActionDispatch
#
# Instead of:
#
- # foo_url(:bar => bar, :baz => baz, :bang => bang)
+ # foo_url(bar: bar, baz: baz, bang: bang)
#
# Also allow options hash, so you can do:
#
- # foo_url(bar, baz, bang, :sort_by => 'baz')
+ # foo_url(bar, baz, bang, sort_by: 'baz')
#
- def define_url_helper(route, name, kind, options)
- selector = url_helper_name(name, kind)
- hash_access_method = hash_access_name(name, kind)
-
- if optimize_helper?(route)
- @module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1
- remove_possible_method :#{selector}
- def #{selector}(*args)
- if args.size == #{route.required_parts.size} && !args.last.is_a?(Hash) && optimize_routes_generation?
- options = #{options.inspect}.merge!(url_options)
- options[:path] = "#{optimized_helper(route)}"
- ActionDispatch::Http::URL.url_for(options)
- else
- url_for(#{hash_access_method}(*args))
- end
+ def define_url_helper(route, name, options)
+ @module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1
+ remove_possible_method :#{name}
+ 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
- END_EVAL
- else
- @module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1
- remove_possible_method :#{selector}
- def #{selector}(*args)
- url_for(#{hash_access_method}(*args))
- end
- END_EVAL
- end
+ end
+ END_EVAL
- helpers << selector
+ helpers << name
end
# Clause check about when we need to generate an optimized helper.
def optimize_helper?(route) #:nodoc:
- route.ast.grep(Journey::Nodes::Star).empty? && route.requirements.except(:controller, :action).empty?
+ route.requirements.except(:controller, :action).empty?
end
# Generates the interpolation to be used in the optimized helper.
@@ -230,7 +216,10 @@ module ActionDispatch
end
route.required_parts.each_with_index do |part, i|
- string_route.gsub!(part.inspect, "\#{Journey::Router::Utils.escape_fragment(args[#{i}].to_param)}")
+ # 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
string_route
@@ -239,7 +228,7 @@ module ActionDispatch
attr_accessor :formatter, :set, :named_routes, :default_scope, :router
attr_accessor :disable_clear_and_finalize, :resources_path_names
- attr_accessor :default_url_options, :request_class, :valid_conditions
+ attr_accessor :default_url_options, :request_class
alias :routes :set
@@ -251,17 +240,7 @@ module ActionDispatch
self.named_routes = NamedRouteCollection.new
self.resources_path_names = self.class.default_resources_path_names.dup
self.default_url_options = {}
-
self.request_class = request_class
- @valid_conditions = {}
-
- request_class.public_instance_methods.each { |m|
- @valid_conditions[m.to_sym] = true
- }
- @valid_conditions[:controller] = true
- @valid_conditions[:action] = true
-
- self.valid_conditions.delete(:id)
@append = []
@prepend = []
@@ -340,9 +319,9 @@ module ActionDispatch
end
end
- MountedHelpers.class_eval <<-RUBY
+ MountedHelpers.class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
def #{name}
- @#{name} ||= _#{name}
+ @_#{name} ||= _#{name}
end
RUBY
end
@@ -392,7 +371,7 @@ module ActionDispatch
raise ArgumentError, "Invalid route name: '#{name}'" unless name.blank? || name.to_s.match(/^[_a-z]\w*$/i)
path = build_path(conditions.delete(:path_info), requirements, SEPARATORS, anchor)
- conditions = build_conditions(conditions, valid_conditions, path.names.map { |x| x.to_sym })
+ 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]
@@ -429,21 +408,22 @@ module ActionDispatch
end
private :build_path
- def build_conditions(current_conditions, req_predicates, path_values)
+ def build_conditions(current_conditions, path_values)
conditions = current_conditions.dup
- verbs = conditions[:request_method] || []
-
# 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('|')}$]
end
- conditions.delete_if { |k,v| !(req_predicates.include?(k) || path_values.include?(k)) }
- conditions
+ conditions.keep_if do |k, _|
+ k == :action || k == :controller ||
+ @request_class.public_method_defined?(k) || path_values.include?(k)
+ end
end
private :build_conditions
@@ -460,22 +440,21 @@ module ActionDispatch
attr_reader :options, :recall, :set, :named_route
- def initialize(options, recall, set, extras = false)
+ def initialize(options, recall, set)
@named_route = options.delete(:use_route)
@options = options.dup
@recall = recall.dup
@set = set
- @extras = extras
normalize_options!
normalize_controller_action_id!
use_relative_controller!
- controller.sub!(%r{^/}, '') if controller
+ normalize_controller!
handle_nil_action!
end
def controller
- @controller ||= @options[:controller]
+ @options[:controller]
end
def current_controller
@@ -484,9 +463,7 @@ module ActionDispatch
def use_recall_for(key)
if @recall[key] && (!@options.key?(key) || @options[key] == @recall[key])
- if named_route_exists?
- @options[key] = @recall.delete(key) if segment_keys.include?(key)
- else
+ if !named_route_exists? || segment_keys.include?(key)
@options[key] = @recall.delete(key)
end
end
@@ -496,7 +473,7 @@ module ActionDispatch
# If an explicit :controller was given, always make :action explicit
# too, so that action expiry works as expected for things like
#
- # generate({:controller => 'content'}, {:controller => 'content', :action => 'show'})
+ # generate({controller: 'content'}, {controller: 'content', action: 'show'})
#
# (the above is from the unit tests). In the above case, because the
# controller was explicitly given, but no action, the action is implied to
@@ -525,19 +502,24 @@ module ActionDispatch
use_recall_for(:id)
end
- # if the current controller is "foo/bar/baz" and :controller => "baz/bat"
+ # if the current controller is "foo/bar/baz" and controller: "baz/bat"
# 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('/')
size = controller.count("/") + 1
parts = old_parts[0...-size] << controller
- @controller = @options[:controller] = parts.join("/")
+ @options[:controller] = parts.join("/")
end
end
- # This handles the case of :action => nil being explicitly passed.
- # It is identical to :action => "index"
+ # Remove leading slashes from controllers
+ def normalize_controller!
+ @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'
@@ -545,20 +527,12 @@ module ActionDispatch
recall[:action] = options.delete(:action) if options[:action] == 'index'
end
+ # Generates a path from routes, returns [path, params]
+ # if no path is returned the formatter will raise Journey::Router::RoutingError
def generate
- path, params = @set.formatter.generate(:path_info, named_route, options, recall, PARAMETERIZE)
-
- raise_routing_error unless path
-
- return [path, params.keys] if @extras
-
- [path, params]
- rescue Journey::Router::RoutingError
- raise_routing_error
- end
-
- def raise_routing_error
- raise ActionController::RoutingError, "No route matches #{options.inspect}"
+ @set.formatter.generate(:path_info, named_route, options, recall, PARAMETERIZE)
+ rescue Journey::Router::RoutingError => e
+ raise ActionController::UrlGenerationError, "No route matches #{options.inspect} #{e.message}"
end
def different_controller?
@@ -583,15 +557,17 @@ module ActionDispatch
end
def generate_extras(options, recall={})
- generate(options, recall, true)
+ path, params = generate(options, recall)
+ return path, params.keys
end
- def generate(options, recall = {}, extras = false)
- Generator.new(options, recall, self, extras).generate
+ def generate(options, recall = {})
+ Generator.new(options, recall, self).generate
end
RESERVED_OPTIONS = [:host, :protocol, :port, :subdomain, :domain, :tld_length,
- :trailing_slash, :anchor, :params, :only_path, :script_name]
+ :trailing_slash, :anchor, :params, :only_path, :script_name,
+ :original_script_name]
def mounted?
false
@@ -605,19 +581,24 @@ module ActionDispatch
nil
end
+ # The +options+ argument must be +nil+ or a hash whose keys are *symbols*.
def url_for(options)
- options = (options || {}).reverse_merge!(default_url_options)
-
- handle_positional_args(options)
+ options = default_url_options.merge(options || {})
user, password = extract_authentication(options)
- path_segments = options.delete(:_path_segments)
- script_name = options.delete(:script_name).presence || _generate_prefix(options)
+ recall = options.delete(:_recall)
+
+ original_script_name = options.delete(:original_script_name).presence
+ script_name = options.delete(:script_name).presence || _generate_prefix(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, params = generate(path_options, path_segments || {})
+ path, params = generate(path_options, recall || {})
params.merge!(options[:params] || {})
ActionDispatch::Http::URL.url_for(options.merge!({
@@ -660,9 +641,13 @@ module ActionDispatch
dispatcher = dispatcher.app
end
- if dispatcher.is_a?(Dispatcher) && dispatcher.controller(params, false)
- dispatcher.prepare_params!(params)
- return params
+ if dispatcher.is_a?(Dispatcher)
+ if dispatcher.controller(params, false)
+ dispatcher.prepare_params!(params)
+ return params
+ else
+ raise ActionController::RoutingError, "A route matches #{path.inspect}, but references missing controller: #{params[:controller].camelize}Controller"
+ end
end
end
@@ -679,16 +664,6 @@ module ActionDispatch
end
end
- def handle_positional_args(options)
- return unless args = options.delete(:_positional_args)
-
- keys = options.delete(:_positional_keys)
- keys -= options.keys if args.size < keys.size - 1 # take format into account
-
- # Tell url_for to skip default_url_options
- options.merge!(Hash[args.zip(keys).map { |v, k| [k, v] }])
- end
-
end
end
end
diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb
index 94db36ce1f..76311c423a 100644
--- a/actionpack/lib/action_dispatch/routing/url_for.rb
+++ b/actionpack/lib/action_dispatch/routing/url_for.rb
@@ -18,8 +18,8 @@ module ActionDispatch
# of parameters. For example, you've probably had the chance to write code
# like this in one of your views:
#
- # <%= link_to('Click here', :controller => 'users',
- # :action => 'new', :message => 'Welcome!') %>
+ # <%= link_to('Click here', controller: 'users',
+ # action: 'new', message: 'Welcome!') %>
# # => "/users/new?message=Welcome%21"
#
# link_to, and all other functions that require URL generation functionality,
@@ -28,22 +28,22 @@ module ActionDispatch
# the same path as the above example by using the following code:
#
# include UrlFor
- # url_for(:controller => 'users',
- # :action => 'new',
- # :message => 'Welcome!',
- # :only_path => true)
+ # url_for(controller: 'users',
+ # action: 'new',
+ # message: 'Welcome!',
+ # only_path: true)
# # => "/users/new?message=Welcome%21"
#
- # Notice the <tt>:only_path => true</tt> part. This is because UrlFor has no
+ # Notice the <tt>only_path: true</tt> part. This is because UrlFor has no
# information about the website hostname that your Rails app is serving. So if you
# want to include the hostname as well, then you must also pass the <tt>:host</tt>
# argument:
#
# include UrlFor
- # url_for(:controller => 'users',
- # :action => 'new',
- # :message => 'Welcome!',
- # :host => 'www.example.com')
+ # url_for(controller: 'users',
+ # action: 'new',
+ # message: 'Welcome!',
+ # host: 'www.example.com')
# # => "http://www.example.com/users/new?message=Welcome%21"
#
# By default, all controllers and views have access to a special version of url_for,
@@ -68,7 +68,7 @@ module ActionDispatch
# This generates, among other things, the method <tt>users_path</tt>. By default,
# this method is accessible from your controllers, views and mailers. If you need
# to access this auto-generated method from other places (such as a model), then
- # you can do that by including ActionController::UrlFor in your class:
+ # you can do that by including Rails.application.routes.url_helpers in your class:
#
# class User < ActiveRecord::Base
# include Rails.application.routes.url_helpers
@@ -95,6 +95,8 @@ module ActionDispatch
self.default_url_options = {}
end
+
+ include(*_url_for_modules) if respond_to?(:_url_for_modules)
end
def initialize(*)
@@ -102,7 +104,7 @@ module ActionDispatch
super
end
- # Hook overriden in controller to add request information
+ # Hook overridden in controller to add request information
# with `default_url_options`. Application logic should not
# go into url_options.
def url_options
@@ -132,22 +134,22 @@ module ActionDispatch
# Any other key (<tt>:controller</tt>, <tt>:action</tt>, etc.) given to
# +url_for+ is forwarded to the Routes module.
#
- # Examples:
- #
- # url_for :controller => 'tasks', :action => 'testing', :host => 'somehost.org', :port => '8080'
+ # url_for controller: 'tasks', action: 'testing', host: 'somehost.org', port: '8080'
# # => 'http://somehost.org:8080/tasks/testing'
- # url_for :controller => 'tasks', :action => 'testing', :host => 'somehost.org', :anchor => 'ok', :only_path => true
+ # url_for controller: 'tasks', action: 'testing', host: 'somehost.org', anchor: 'ok', only_path: true
# # => '/tasks/testing#ok'
- # url_for :controller => 'tasks', :action => 'testing', :trailing_slash => true
+ # url_for controller: 'tasks', action: 'testing', trailing_slash: true
# # => 'http://somehost.org/tasks/testing/'
- # url_for :controller => 'tasks', :action => 'testing', :host => 'somehost.org', :number => '33'
+ # url_for controller: 'tasks', action: 'testing', host: 'somehost.org', number: '33'
# # => 'http://somehost.org/tasks/testing?number=33'
def url_for(options = nil)
case options
+ when nil
+ _routes.url_for(url_options.symbolize_keys)
+ when Hash
+ _routes.url_for(options.symbolize_keys.reverse_merge!(url_options))
when String
options
- when nil, Hash
- _routes.url_for((options || {}).symbolize_keys.reverse_merge!(url_options))
else
polymorphic_url(options)
end