Skip to content

Commit

Permalink
Add support for calling nested direct routes (#28462)
Browse files Browse the repository at this point in the history
Not all requirements can be expressed in terms of polymorphic url
options so add a `route_for` method that allows calling another
direct route (or regular named route) which a set of arguments, e.g:

    resources :buckets

    direct :recordable do |recording|
      route_for(:bucket, recording.bucket)
    end

    direct :threadable do |threadable|
      route_for(:recordable, threadable.parent)
    end

This maintains the context of the original caller, e.g.

    threadable_path(threadable) # => /buckets/1
    threadable_url(threadable)  # => http://example.com/buckets/1
  • Loading branch information
pixeltrix authored and dhh committed Mar 17, 2017
1 parent 7413be0 commit 35afd2c
Show file tree
Hide file tree
Showing 4 changed files with 48 additions and 32 deletions.
8 changes: 4 additions & 4 deletions actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
Expand Up @@ -40,7 +40,7 @@ module Routing
#
# Example usage:
#
# edit_polymorphic_path(@post) # => "/posts/1/edit"
# edit_polymorphic_path(@post) # => "/posts/1/edit"
# polymorphic_path(@post, format: :pdf) # => "/posts/1.pdf"
#
# == Usage with mounted engines
Expand Down Expand Up @@ -104,7 +104,7 @@ def polymorphic_url(record_or_hash_or_array, options = {})
end

if mapping = polymorphic_mapping(record_or_hash_or_array)
return mapping.call(self, [record_or_hash_or_array, options])
return mapping.call(self, [record_or_hash_or_array, options], false)
end

opts = options.dup
Expand All @@ -128,7 +128,7 @@ def polymorphic_path(record_or_hash_or_array, options = {})
end

if mapping = polymorphic_mapping(record_or_hash_or_array)
return mapping.call(self, [record_or_hash_or_array, options], only_path: true)
return mapping.call(self, [record_or_hash_or_array, options], true)
end

opts = options.dup
Expand Down Expand Up @@ -273,7 +273,7 @@ def handle_model(record)

def handle_model_call(target, record)
if mapping = polymorphic_mapping(target, record)
mapping.call(target, [record], only_path: suffix == "path")
mapping.call(target, [record], suffix == "path")
else
method, args = handle_model(record)
target.send(method, *args)
Expand Down
32 changes: 12 additions & 20 deletions actionpack/lib/action_dispatch/routing/route_set.rb
Expand Up @@ -164,13 +164,13 @@ def add_url_helper(name, defaults, &block)

@path_helpers_module.module_eval do
define_method(:"#{name}_path") do |*args|
helper.call(self, args, only_path: true)
helper.call(self, args, true)
end
end

@url_helpers_module.module_eval do
define_method(:"#{name}_url") do |*args|
helper.call(self, args)
helper.call(self, args, false)
end
end
end
Expand Down Expand Up @@ -509,6 +509,10 @@ def url_for(options)
@_proxy.url_for(options)
end

def route_for(name, *args)
@_proxy.route_for(name, *args)
end

def optimize_routes_generation?
@_proxy.optimize_routes_generation?
end
Expand Down Expand Up @@ -613,26 +617,14 @@ def initialize(name, defaults, &block)
@block = block
end

def call(t, args, outer_options = {})
def call(t, args, only_path = false)
options = args.extract_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 a 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)))
url = t.url_for(eval_block(t, args, options))

if only_path
"/" + url.partition(%r{(?<!/)/(?!/)}).last
else
t.url_for([url_options, outer_options])
url
end
end

Expand Down
4 changes: 4 additions & 0 deletions actionpack/lib/action_dispatch/routing/url_for.rb
Expand Up @@ -192,6 +192,10 @@ def url_for(options = nil)
end
end

def route_for(name, *args) # :nodoc:
public_send(:"#{name}_url", *args)
end

protected

def optimize_routes_generation?
Expand Down
36 changes: 28 additions & 8 deletions actionpack/test/dispatch/routing/custom_url_helpers_test.rb
Expand Up @@ -4,18 +4,23 @@ class TestCustomUrlHelpers < ActionDispatch::IntegrationTest
class Linkable
attr_reader :id

def self.name
super.demodulize
end

def initialize(id)
@id = id
end

def linkable_type
self.class.name.demodulize.underscore
self.class.name.underscore
end
end

class Category < Linkable; end
class Collection < Linkable; end
class Product < Linkable; end
class Manufacturer < Linkable; end

class Model
extend ActiveModel::Naming
Expand Down Expand Up @@ -79,7 +84,7 @@ class ProductPage < Page; end
get "/media/:id", to: "media#show", as: :media
get "/pages/:id", to: "pages#show", as: :page

resources :categories, :collections, :products
resources :categories, :collections, :products, :manufacturers

namespace :admin do
get "/dashboard", to: "dashboard#index"
Expand All @@ -89,6 +94,7 @@ class ProductPage < Page; end
direct("string") { "http://www.rubyonrails.org" }
direct(:helper) { basket_url }
direct(:linkable) { |linkable| [:"#{linkable.linkable_type}", { id: linkable.id }] }
direct(:nested) { |linkable| route_for(:linkable, linkable) }
direct(:params) { |params| params }
direct(:symbol) { :basket }
direct(:hash) { { controller: "basket", action: "show" } }
Expand All @@ -102,6 +108,7 @@ class ProductPage < Page; end

resolve("Article") { |article| [:post, { id: article.id }] }
resolve("Basket") { |basket| [:basket] }
resolve("Manufacturer") { |manufacturer| route_for(:linkable, manufacturer) }
resolve("User", anchor: "details") { |user, options| [:profile, options] }
resolve("Video") { |video| [:media, { id: video.id }] }
resolve(%w[Page CategoryPage ProductPage]) { |page| [:page, { id: page.id }] }
Expand All @@ -119,6 +126,7 @@ def setup
@category = Category.new("1")
@collection = Collection.new("2")
@product = Product.new("3")
@manufacturer = Manufacturer.new("apple")
@basket = Basket.new
@user = User.new
@video = Video.new("4")
Expand All @@ -136,14 +144,14 @@ def params
end

def test_direct_paths
assert_equal "http://www.rubyonrails.org", website_path
assert_equal "http://www.rubyonrails.org", Routes.url_helpers.website_path
assert_equal "/", website_path
assert_equal "/", Routes.url_helpers.website_path

assert_equal "http://www.rubyonrails.org", string_path
assert_equal "http://www.rubyonrails.org", Routes.url_helpers.string_path
assert_equal "/", string_path
assert_equal "/", Routes.url_helpers.string_path

assert_equal "http://www.example.com/basket", helper_url
assert_equal "http://www.example.com/basket", Routes.url_helpers.helper_url
assert_equal "/basket", helper_path
assert_equal "/basket", Routes.url_helpers.helper_path

assert_equal "/categories/1", linkable_path(@category)
assert_equal "/categories/1", Routes.url_helpers.linkable_path(@category)
Expand All @@ -152,6 +160,9 @@ def test_direct_paths
assert_equal "/products/3", linkable_path(@product)
assert_equal "/products/3", Routes.url_helpers.linkable_path(@product)

assert_equal "/categories/1", nested_path(@category)
assert_equal "/categories/1", Routes.url_helpers.nested_path(@category)

assert_equal "/", params_path(@safe_params)
assert_equal "/", Routes.url_helpers.params_path(@safe_params)
assert_raises(ArgumentError) { params_path(@unsafe_params) }
Expand Down Expand Up @@ -192,6 +203,9 @@ def test_direct_urls
assert_equal "http://www.example.com/products/3", linkable_url(@product)
assert_equal "http://www.example.com/products/3", Routes.url_helpers.linkable_url(@product)

assert_equal "http://www.example.com/categories/1", nested_url(@category)
assert_equal "http://www.example.com/categories/1", Routes.url_helpers.nested_url(@category)

assert_equal "http://www.example.com/", params_url(@safe_params)
assert_equal "http://www.example.com/", Routes.url_helpers.params_url(@safe_params)
assert_raises(ArgumentError) { params_url(@unsafe_params) }
Expand Down Expand Up @@ -244,6 +258,9 @@ def test_resolve_paths
assert_equal "/pages/8", polymorphic_path(@product_page)
assert_equal "/pages/8", Routes.url_helpers.polymorphic_path(@product_page)
assert_equal "/pages/8", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.path.handle_model_call(self, @product_page)

assert_equal "/manufacturers/apple", polymorphic_path(@manufacturer)
assert_equal "/manufacturers/apple", Routes.url_helpers.polymorphic_path(@manufacturer)
end

def test_resolve_urls
Expand Down Expand Up @@ -277,6 +294,9 @@ def test_resolve_urls
assert_equal "http://www.example.com/pages/8", polymorphic_url(@product_page)
assert_equal "http://www.example.com/pages/8", Routes.url_helpers.polymorphic_url(@product_page)
assert_equal "http://www.example.com/pages/8", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.url.handle_model_call(self, @product_page)

assert_equal "http://www.example.com/manufacturers/apple", polymorphic_url(@manufacturer)
assert_equal "http://www.example.com/manufacturers/apple", Routes.url_helpers.polymorphic_url(@manufacturer)
end

def test_defining_direct_inside_a_scope_raises_runtime_error
Expand Down

0 comments on commit 35afd2c

Please sign in to comment.