aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--actionpack/lib/action_dispatch/routing/mapper.rb64
-rw-r--r--actionpack/lib/action_dispatch/routing/polymorphic_routes.rb27
-rw-r--r--actionpack/lib/action_dispatch/routing/route_set.rb106
-rw-r--r--actionpack/test/dispatch/routing/direct_url_helpers_test.rb (renamed from actionpack/test/dispatch/routing/custom_url_helpers_test.rb)72
-rw-r--r--actionview/lib/action_view/test_case.rb8
-rw-r--r--railties/test/application/routing_test.rb102
6 files changed, 314 insertions, 65 deletions
diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb
index 3756ef15a2..6123a1f5f5 100644
--- a/actionpack/lib/action_dispatch/routing/mapper.rb
+++ b/actionpack/lib/action_dispatch/routing/mapper.rb
@@ -2021,8 +2021,8 @@ module ActionDispatch
end
module DirectUrls
- # Define a custom url helper that will be added to the url helpers
- # module. This allows you override and/or replace the default behavior
+ # Define custom routing behavior that will be added to the application's
+ # routes. This allows you override and/or replace the default behavior
# of routing helpers, e.g:
#
# direct :homepage do
@@ -2037,8 +2037,35 @@ module ActionDispatch
# { controller: 'pages', action: 'index', subdomain: 'www' }
# end
#
- # The return value must be a valid set of arguments for `url_for` which
- # will actually build the url string. This can be one of the following:
+ # The above example show how to define a custom url helper but it's also
+ # possible to alter the behavior of `polymorphic_url` and consequently the
+ # behavior of `link_to` and `form_for` when passed a model instance, e.g:
+ #
+ # direct class: "Basket" do
+ # [:basket]
+ # end
+ #
+ # NOTE: This custom behavior only applies to simple polymorphic urls where
+ # a single model instance is passed and not more complicated forms, e.g:
+ #
+ # # config/routes.rb
+ # resource :profile
+ # namespace :admin do
+ # resources :users
+ # end
+ #
+ # direct(class: "User") { [:profile] }
+ #
+ # # app/views/application/_menu.html.erb
+ # link_to 'Profile', @current_user
+ # link_to 'Profile', [:admin, @current_user]
+ #
+ # The first `link_to` will generate '/profile' but the second will generate
+ # the standard polymorphic url of '/admin/users/1'.
+ #
+ # The return value from the block passed to `direct` must be a valid set of
+ # arguments for `url_for` which will actually build the url string. This can
+ # be one of the following:
#
# * A string, which is treated as a generated url
# * A hash, e.g. { controller: 'pages', action: 'index' }
@@ -2046,6 +2073,9 @@ module ActionDispatch
# * An Active Model instance
# * An Active Model class
#
+ # NOTE: Other url helpers can be called in the block but be careful not to invoke
+ # your custom url helper again otherwise it will result in a stack overflow error
+ #
# You can also specify default options that will be passed through to
# your url helper definition, e.g:
#
@@ -2053,14 +2083,28 @@ module ActionDispatch
# [ :products, options.merge(params.permit(:page, :size)) ]
# end
#
- # NOTE: It is the url helper's responsibility to return the correct
- # set of options to be passed to the `url_for` call.
- def direct(name, options = nil, &block)
- case name
+ # You can pass options to a polymorphic mapping do - the arity for the block
+ # needs to be two as the instance is passed as the first argument, e.g:
+ #
+ # direct class: 'Basket', anchor: 'items' do |basket, options|
+ # [:basket, options]
+ # end
+ #
+ # This generates the url '/basket#items' because when the last item in an
+ # array passed to `polymorphic_url` is a hash then it's treated as options
+ # to the url helper that gets called.
+ #
+ # NOTE: The `direct` method doesn't observe the current scope in routes.rb
+ # and because of this it's recommended to define them outside of any blocks
+ # such as `namespace` or `scope`.
+ def direct(name_or_hash, options = nil, &block)
+ case name_or_hash
+ when Hash
+ @set.add_polymorphic_mapping(name_or_hash, &block)
when String, Symbol
- @set.add_url_helper(name, options, &block)
+ @set.add_url_helper(name_or_hash, options, &block)
else
- raise ArgumentError, "The direct method only accepts a string or symbol"
+ raise ArgumentError, "The direct method only accepts a hash, string or symbol"
end
end
end
diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
index 432b9bf4c1..512e23c833 100644
--- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
+++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
@@ -103,6 +103,10 @@ module ActionDispatch
return polymorphic_url record, options
end
+ if mapping = polymorphic_mapping(record_or_hash_or_array)
+ return mapping.call(self, [record_or_hash_or_array], options)
+ end
+
opts = options.dup
action = opts.delete :action
type = opts.delete(:routing_type) || :url
@@ -123,6 +127,10 @@ module ActionDispatch
return polymorphic_path record, options
end
+ if mapping = polymorphic_mapping(record_or_hash_or_array)
+ return mapping.call(self, [record_or_hash_or_array], options, only_path: true)
+ end
+
opts = options.dup
action = opts.delete :action
type = :path
@@ -156,6 +164,11 @@ module ActionDispatch
polymorphic_path(record_or_hash, options.merge(action: action))
end
+ def polymorphic_mapping(record)
+ return false unless record.respond_to?(:to_model)
+ _routes.polymorphic_mappings[record.to_model.model_name.name]
+ end
+
class HelperMethodBuilder # :nodoc:
CACHE = { "path" => {}, "url" => {} }
@@ -255,9 +268,13 @@ module ActionDispatch
[named_route, args]
end
- def handle_model_call(target, model)
- method, args = handle_model model
- target.send(method, *args)
+ def handle_model_call(target, record)
+ if mapping = polymorphic_mapping(target, record)
+ mapping.call(target, [record], {}, only_path: suffix == "path")
+ else
+ method, args = handle_model(record)
+ target.send(method, *args)
+ end
end
def handle_list(list)
@@ -303,6 +320,10 @@ module ActionDispatch
private
+ def polymorphic_mapping(target, record)
+ target._routes.polymorphic_mappings[record.to_model.model_name.name]
+ end
+
def get_method_for_class(klass)
name = @key_strategy.call klass.model_name
get_method_for_string name
diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb
index 923d063655..8bdf0d1a53 100644
--- a/actionpack/lib/action_dispatch/routing/route_set.rb
+++ b/actionpack/lib/action_dispatch/routing/route_set.rb
@@ -160,7 +160,7 @@ module ActionDispatch
def add_url_helper(name, defaults, &block)
@custom_helpers << name
- helper = CustomUrlHelper.new(name, defaults, &block)
+ helper = DirectUrlHelper.new(name, defaults, &block)
@path_helpers_module.module_eval do
define_method(:"#{name}_path") do |*args|
@@ -177,47 +177,6 @@ module ActionDispatch
end
end
- class CustomUrlHelper
- attr_reader :name, :defaults, :block
-
- def initialize(name, defaults, &block)
- @name = name
- @defaults = defaults
- @block = block
- end
-
- def call(t, args, options, outer_options = {})
- url_options = eval_block(t, args, options)
-
- case url_options
- when String
- t.url_for(url_options)
- when Hash
- t.url_for(url_options.merge(outer_options))
- when ActionController::Parameters
- if url_options.permitted?
- t.url_for(url_options.to_h.merge(outer_options))
- else
- raise ArgumentError, "Generating an URL from non sanitized request parameters is insecure!"
- end
- when Array
- opts = url_options.extract_options!
- t.url_for(url_options.push(opts.merge(outer_options)))
- else
- t.url_for([url_options, outer_options])
- end
- end
-
- private
- def eval_block(t, args, options)
- t.instance_exec(*args, merge_defaults(options), &block)
- end
-
- def merge_defaults(options)
- defaults ? defaults.merge(options) : options
- end
- end
-
class UrlHelper
def self.create(route, options, route_name, url_strategy)
if optimize_helper?(route)
@@ -380,7 +339,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
- attr_reader :env_key
+ attr_reader :env_key, :polymorphic_mappings
alias :routes :set
@@ -422,6 +381,7 @@ module ActionDispatch
@set = Journey::Routes.new
@router = Journey::Router.new @set
@formatter = Journey::Formatter.new self
+ @polymorphic_mappings = {}
end
def eager_load!
@@ -483,6 +443,7 @@ module ActionDispatch
named_routes.clear
set.clear
formatter.clear
+ @polymorphic_mappings.clear
@prepend.each { |blk| eval_block(blk) }
end
@@ -554,6 +515,14 @@ module ActionDispatch
@_proxy.optimize_routes_generation?
end
+ def polymorphic_url(record_or_hash_or_array, options = {})
+ @_proxy.polymorphic_url(record_or_hash_or_array, options)
+ end
+
+ def polymorphic_path(record_or_hash_or_array, options = {})
+ @_proxy.polymorphic_path(record_or_hash_or_array, options)
+ end
+
def _routes; @_proxy._routes; end
def url_options; {}; end
end
@@ -629,10 +598,61 @@ module ActionDispatch
route
end
+ def add_polymorphic_mapping(options, &block)
+ defaults = options.dup
+ klass = defaults.delete(:class)
+ if klass.nil?
+ raise ArgumentError, "Missing :class key from polymorphic mapping options"
+ end
+
+ @polymorphic_mappings[klass] = DirectUrlHelper.new(klass, defaults, &block)
+ end
+
def add_url_helper(name, options, &block)
named_routes.add_url_helper(name, options, &block)
end
+ class DirectUrlHelper
+ attr_reader :name, :defaults, :block
+
+ def initialize(name, defaults, &block)
+ @name = name
+ @defaults = defaults
+ @block = block
+ end
+
+ def call(t, args, options, outer_options = {})
+ url_options = eval_block(t, args, options)
+
+ case url_options
+ when String
+ t.url_for(url_options)
+ when Hash
+ t.url_for(url_options.merge(outer_options))
+ when ActionController::Parameters
+ if url_options.permitted?
+ t.url_for(url_options.to_h.merge(outer_options))
+ else
+ raise ArgumentError, "Generating an URL from non sanitized request parameters is insecure!"
+ end
+ when Array
+ opts = url_options.extract_options!
+ t.url_for(url_options.push(opts.merge(outer_options)))
+ else
+ t.url_for([url_options, outer_options])
+ end
+ end
+
+ private
+ def eval_block(t, args, options)
+ t.instance_exec(*args, merge_defaults(options), &block)
+ end
+
+ def merge_defaults(options)
+ defaults ? defaults.merge(options) : options
+ end
+ end
+
class Generator
PARAMETERIZE = lambda do |name, value|
if name == :controller
diff --git a/actionpack/test/dispatch/routing/custom_url_helpers_test.rb b/actionpack/test/dispatch/routing/direct_url_helpers_test.rb
index 22e8bbf21e..56bb7f13b3 100644
--- a/actionpack/test/dispatch/routing/custom_url_helpers_test.rb
+++ b/actionpack/test/dispatch/routing/direct_url_helpers_test.rb
@@ -1,6 +1,6 @@
require "abstract_unit"
-class TestCustomUrlHelpers < ActionDispatch::IntegrationTest
+class TestDirectUrlHelpers < ActionDispatch::IntegrationTest
class Linkable
attr_reader :id
@@ -17,12 +17,37 @@ class TestCustomUrlHelpers < ActionDispatch::IntegrationTest
class Collection < Linkable; end
class Product < Linkable; end
+ class Model
+ extend ActiveModel::Naming
+ include ActiveModel::Conversion
+
+ attr_reader :id
+
+ def initialize(id = nil)
+ @id = id
+ end
+
+ def model_name
+ @_model_name ||= ActiveModel::Name.new(self.class, nil, self.class.name.demodulize)
+ end
+
+ def persisted?
+ false
+ end
+ end
+
+ class Basket < Model; end
+ class User < Model; end
+ class Video < Model; end
+
Routes = ActionDispatch::Routing::RouteSet.new
Routes.draw do
default_url_options host: "www.example.com"
root to: "pages#index"
get "/basket", to: "basket#show", as: :basket
+ get "/profile", to: "users#profile", as: :profile
+ get "/media/:id", to: "media#show", as: :media
resources :categories, :collections, :products
@@ -40,6 +65,10 @@ class TestCustomUrlHelpers < ActionDispatch::IntegrationTest
direct(:array) { [:admin, :dashboard] }
direct(:options) { |options| [:products, options] }
direct(:defaults, size: 10) { |options| [:products, options] }
+
+ direct(class: "Basket") { |basket| [:basket] }
+ direct(class: "User", anchor: "details") { |user, options| [:profile, options] }
+ direct(class: "Video") { |video| [:media, { id: video.id }] }
end
APP = build_app Routes
@@ -54,6 +83,9 @@ class TestCustomUrlHelpers < ActionDispatch::IntegrationTest
@category = Category.new("1")
@collection = Collection.new("2")
@product = Product.new("3")
+ @basket = Basket.new
+ @user = User.new
+ @video = Video.new("4")
@path_params = { "controller" => "pages", "action" => "index" }
@unsafe_params = ActionController::Parameters.new(@path_params)
@safe_params = ActionController::Parameters.new(@path_params).permit(:controller, :action)
@@ -94,6 +126,19 @@ class TestCustomUrlHelpers < ActionDispatch::IntegrationTest
assert_equal "/products?size=10", Routes.url_helpers.defaults_path
assert_equal "/products?size=20", defaults_path(size: 20)
assert_equal "/products?size=20", Routes.url_helpers.defaults_path(size: 20)
+
+ assert_equal "/basket", polymorphic_path(@basket)
+ assert_equal "/basket", Routes.url_helpers.polymorphic_path(@basket)
+
+ assert_equal "/profile#details", polymorphic_path(@user)
+ assert_equal "/profile#details", Routes.url_helpers.polymorphic_path(@user)
+
+ assert_equal "/profile#password", polymorphic_path(@user, anchor: "password")
+ assert_equal "/profile#password", Routes.url_helpers.polymorphic_path(@user, anchor: "password")
+
+ assert_equal "/media/4", polymorphic_path(@video)
+ assert_equal "/media/4", Routes.url_helpers.polymorphic_path(@video)
+ assert_equal "/media/4", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.path.handle_model_call(self, @video)
end
def test_direct_urls
@@ -131,6 +176,21 @@ class TestCustomUrlHelpers < ActionDispatch::IntegrationTest
assert_equal "http://www.example.com/products?size=10", Routes.url_helpers.defaults_url
assert_equal "http://www.example.com/products?size=20", defaults_url(size: 20)
assert_equal "http://www.example.com/products?size=20", Routes.url_helpers.defaults_url(size: 20)
+
+ assert_equal "http://www.example.com/basket", polymorphic_url(@basket)
+ assert_equal "http://www.example.com/basket", Routes.url_helpers.polymorphic_url(@basket)
+ assert_equal "http://www.example.com/basket", polymorphic_url(@basket)
+ assert_equal "http://www.example.com/basket", Routes.url_helpers.polymorphic_url(@basket)
+
+ assert_equal "http://www.example.com/profile#details", polymorphic_url(@user)
+ assert_equal "http://www.example.com/profile#details", Routes.url_helpers.polymorphic_url(@user)
+
+ assert_equal "http://www.example.com/profile#password", polymorphic_url(@user, anchor: "password")
+ assert_equal "http://www.example.com/profile#password", Routes.url_helpers.polymorphic_url(@user, anchor: "password")
+
+ assert_equal "http://www.example.com/media/4", polymorphic_url(@video)
+ assert_equal "http://www.example.com/media/4", Routes.url_helpers.polymorphic_url(@video)
+ assert_equal "http://www.example.com/media/4", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.url.handle_model_call(self, @video)
end
def test_raises_argument_error
@@ -142,4 +202,14 @@ class TestCustomUrlHelpers < ActionDispatch::IntegrationTest
end
end
end
+
+ def test_missing_class_raises_argument_error
+ routes = ActionDispatch::Routing::RouteSet.new
+
+ assert_raises ArgumentError do
+ routes.draw do
+ direct(fragment: "core") { "http://www.rubyonrails.org" }
+ end
+ end
+ end
end
diff --git a/actionview/lib/action_view/test_case.rb b/actionview/lib/action_view/test_case.rb
index 2b981caa65..ae4fec4337 100644
--- a/actionview/lib/action_view/test_case.rb
+++ b/actionview/lib/action_view/test_case.rb
@@ -124,6 +124,10 @@ module ActionView
@_rendered_views ||= RenderedViewsCollection.new
end
+ def _routes
+ @controller._routes if @controller.respond_to?(:_routes)
+ end
+
# Need to experiment if this priority is the best one: rendered => output_buffer
class RenderedViewsCollection
def initialize
@@ -258,10 +262,6 @@ module ActionView
end]
end
- def _routes
- @controller._routes if @controller.respond_to?(:_routes)
- end
-
def method_missing(selector, *args)
begin
routes = @controller.respond_to?(:_routes) && @controller._routes
diff --git a/railties/test/application/routing_test.rb b/railties/test/application/routing_test.rb
index 98673f5c1a..6c34c72564 100644
--- a/railties/test/application/routing_test.rb
+++ b/railties/test/application/routing_test.rb
@@ -264,9 +264,9 @@ module ApplicationTests
end
{
- "development" => ["baz", "http://www.apple.com"],
- "production" => ["bar", "http://www.microsoft.com"]
- }.each do |mode, (expected_action, expected_url)|
+ "development" => ["baz", "http://www.apple.com", "/dashboard"],
+ "production" => ["bar", "http://www.microsoft.com", "/profile"]
+ }.each do |mode, (expected_action, expected_url, expected_mapping)|
test "reloads routes when configuration is changed in #{mode}" do
controller :foo, <<-RUBY
class FooController < ApplicationController
@@ -281,6 +281,25 @@ module ApplicationTests
def custom
render plain: custom_url
end
+
+ def mapping
+ render plain: url_for(User.new)
+ end
+ end
+ RUBY
+
+ app_file "app/models/user.rb", <<-RUBY
+ class User
+ extend ActiveModel::Naming
+ include ActiveModel::Conversion
+
+ def model_name
+ @_model_name ||= ActiveModel::Name.new(self.class, nil, "User")
+ end
+
+ def persisted?
+ false
+ end
end
RUBY
@@ -288,8 +307,10 @@ module ApplicationTests
Rails.application.routes.draw do
get 'foo', to: 'foo#bar'
get 'custom', to: 'foo#custom'
+ get 'mapping', to: 'foo#mapping'
direct(:custom) { "http://www.microsoft.com" }
+ direct(class: "User") { "/profile" }
end
RUBY
@@ -301,12 +322,17 @@ module ApplicationTests
get "/custom"
assert_equal "http://www.microsoft.com", last_response.body
+ get "/mapping"
+ assert_equal "/profile", last_response.body
+
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get 'foo', to: 'foo#baz'
get 'custom', to: 'foo#custom'
+ get 'mapping', to: 'foo#mapping'
direct(:custom) { "http://www.apple.com" }
+ direct(class: "User") { "/dashboard" }
end
RUBY
@@ -317,6 +343,9 @@ module ApplicationTests
get "/custom"
assert_equal expected_url, last_response.body
+
+ get "/mapping"
+ assert_equal expected_mapping, last_response.body
end
end
@@ -379,7 +408,11 @@ module ApplicationTests
end
def custom
- render text: custom_url
+ render plain: custom_url
+ end
+
+ def mapping
+ render plain: url_for(User.new)
end
end
RUBY
@@ -392,6 +425,21 @@ module ApplicationTests
end
RUBY
+ app_file "app/models/user.rb", <<-RUBY
+ class User
+ extend ActiveModel::Naming
+ include ActiveModel::Conversion
+
+ def model_name
+ @_model_name ||= ActiveModel::Name.new(self.class, nil, "User")
+ end
+
+ def persisted?
+ false
+ end
+ end
+ RUBY
+
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get 'foo', to: 'foo#index'
@@ -412,6 +460,12 @@ module ApplicationTests
Rails.application.routes.draw do
get 'foo', to: 'foo#index'
get 'bar', to: 'bar#index'
+
+ get 'custom', to: 'foo#custom'
+ direct(:custom) { 'http://www.apple.com' }
+
+ get 'mapping', to: 'foo#mapping'
+ direct(class: 'User') { '/profile' }
end
RUBY
@@ -425,6 +479,14 @@ module ApplicationTests
assert_equal "bar", last_response.body
assert_equal "/bar", Rails.application.routes.url_helpers.bar_path
+ get "/custom"
+ assert_equal "http://www.apple.com", last_response.body
+ assert_equal "http://www.apple.com", Rails.application.routes.url_helpers.custom_url
+
+ get "/mapping"
+ assert_equal "/profile", last_response.body
+ assert_equal "/profile", Rails.application.routes.url_helpers.polymorphic_path(User.new)
+
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get 'foo', to: 'foo#index'
@@ -442,6 +504,18 @@ module ApplicationTests
assert_raises NoMethodError do
assert_equal "/bar", Rails.application.routes.url_helpers.bar_path
end
+
+ get "/custom"
+ assert_equal 404, last_response.status
+ assert_raises NoMethodError do
+ assert_equal "http://www.apple.com", Rails.application.routes.url_helpers.custom_url
+ end
+
+ get "/mapping"
+ assert_equal 404, last_response.status
+ assert_raises NoMethodError do
+ assert_equal "/profile", Rails.application.routes.url_helpers.polymorphic_path(User.new)
+ end
end
test "named routes are cleared when reloading" do
@@ -463,10 +537,27 @@ module ApplicationTests
end
RUBY
+ app_file "app/models/user.rb", <<-RUBY
+ class User
+ extend ActiveModel::Naming
+ include ActiveModel::Conversion
+
+ def model_name
+ @_model_name ||= ActiveModel::Name.new(self.class, nil, "User")
+ end
+
+ def persisted?
+ false
+ end
+ end
+ RUBY
+
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get ':locale/foo', to: 'foo#index', as: 'foo'
+ get 'users', to: 'foo#users', as: 'users'
direct(:microsoft) { 'http://www.microsoft.com' }
+ direct(class: 'User') { '/profile' }
end
RUBY
@@ -474,10 +565,12 @@ module ApplicationTests
assert_equal "foo", last_response.body
assert_equal "/en/foo", Rails.application.routes.url_helpers.foo_path(locale: "en")
assert_equal "http://www.microsoft.com", Rails.application.routes.url_helpers.microsoft_url
+ assert_equal "/profile", Rails.application.routes.url_helpers.polymorphic_path(User.new)
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get ':locale/bar', to: 'bar#index', as: 'foo'
+ get 'users', to: 'foo#users', as: 'users'
direct(:apple) { 'http://www.apple.com' }
end
RUBY
@@ -491,6 +584,7 @@ module ApplicationTests
assert_equal "bar", last_response.body
assert_equal "/en/bar", Rails.application.routes.url_helpers.foo_path(locale: "en")
assert_equal "http://www.apple.com", Rails.application.routes.url_helpers.apple_url
+ assert_equal "/users", Rails.application.routes.url_helpers.polymorphic_path(User.new)
assert_raises NoMethodError do
assert_equal "http://www.microsoft.com", Rails.application.routes.url_helpers.microsoft_url