aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--actionpack/lib/action_dispatch/routing/mapper.rb41
-rw-r--r--actionpack/lib/action_dispatch/routing/route_set.rb75
-rw-r--r--actionpack/test/dispatch/routing/custom_url_helpers_test.rb121
-rw-r--r--railties/test/application/routing_test.rb35
4 files changed, 270 insertions, 2 deletions
diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb
index 8d9f70e3c6..329a374d1e 100644
--- a/actionpack/lib/action_dispatch/routing/mapper.rb
+++ b/actionpack/lib/action_dispatch/routing/mapper.rb
@@ -2020,6 +2020,46 @@ module ActionDispatch
end
end
+ module UrlHelpers
+ # Define a custom url helper that will be added to the url helpers
+ # module. This allows you override and/or replace the default behavior
+ # of routing helpers, e.g:
+ #
+ # url_helper :homepage do
+ # "http://www.rubyonrails.org"
+ # end
+ #
+ # url_helper :commentable do |model|
+ # [ model, anchor: model.dom_id ]
+ # end
+ #
+ # url_helper :main do
+ # { 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:
+ #
+ # * A string, which is treated as a generated url
+ # * A hash, e.g. { controller: 'pages', action: 'index' }
+ # * An array, which is passed to `polymorphic_url`
+ # * An Active Model instance
+ # * An Active Model class
+ #
+ # You can also specify default options that will be passed through to
+ # your url helper definition, e.g:
+ #
+ # url_helper :browse, page: 1, size: 10 do |options|
+ # [ :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 url_helper(name, options = {}, &block)
+ @set.add_url_helper(name, options, &block)
+ end
+ end
+
class Scope # :nodoc:
OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module,
:controller, :action, :path_names, :constraints,
@@ -2113,6 +2153,7 @@ module ActionDispatch
include Scoping
include Concerns
include Resources
+ include UrlHelpers
end
end
end
diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb
index 8ccfab56cf..b1f7cd30fc 100644
--- a/actionpack/lib/action_dispatch/routing/route_set.rb
+++ b/actionpack/lib/action_dispatch/routing/route_set.rb
@@ -73,6 +73,7 @@ module ActionDispatch
@routes = {}
@path_helpers = Set.new
@url_helpers = Set.new
+ @custom_helpers = Set.new
@url_helpers_module = Module.new
@path_helpers_module = Module.new
end
@@ -95,9 +96,23 @@ module ActionDispatch
@url_helpers_module.send :undef_method, helper
end
+ @custom_helpers.each do |helper|
+ path_name = :"#{helper}_path"
+ url_name = :"#{helper}_url"
+
+ if @path_helpers_module.method_defined?(path_name)
+ @path_helpers_module.send :undef_method, path_name
+ end
+
+ if @url_helpers_module.method_defined?(url_name)
+ @url_helpers_module.send :undef_method, url_name
+ end
+ end
+
@routes.clear
@path_helpers.clear
@url_helpers.clear
+ @custom_helpers.clear
end
def add(name, route)
@@ -143,6 +158,62 @@ module ActionDispatch
routes.length
end
+ def add_url_helper(name, defaults, &block)
+ @custom_helpers << name
+ helper = CustomUrlHelper.new(name, defaults, &block)
+
+ @path_helpers_module.module_eval do
+ define_method(:"#{name}_path") do |*args|
+ options = args.extract_options!
+ helper.call(self, args, options, only_path: true)
+ end
+ end
+
+ @url_helpers_module.module_eval do
+ define_method(:"#{name}_url") do |*args|
+ options = args.extract_options!
+ helper.call(self, args, options)
+ end
+ 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, defaults.merge(options), &block)
+ end
+ end
+
class UrlHelper
def self.create(route, options, route_name, url_strategy)
if optimize_helper?(route)
@@ -554,6 +625,10 @@ module ActionDispatch
route
end
+ def add_url_helper(name, options, &block)
+ named_routes.add_url_helper(name, options, &block)
+ 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/custom_url_helpers_test.rb
new file mode 100644
index 0000000000..ec0484a3ba
--- /dev/null
+++ b/actionpack/test/dispatch/routing/custom_url_helpers_test.rb
@@ -0,0 +1,121 @@
+require "abstract_unit"
+
+class TestCustomUrlHelpers < ActionDispatch::IntegrationTest
+ class Linkable
+ attr_reader :id
+
+ def initialize(id)
+ @id = id
+ end
+
+ def linkable_type
+ self.class.name.demodulize.underscore
+ end
+ end
+
+ class Category < Linkable; end
+ class Collection < Linkable; end
+ class Product < Linkable; 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
+
+ resources :categories, :collections, :products
+
+ namespace :admin do
+ get "/dashboard", to: "dashboard#index"
+ end
+
+ url_helper(:website) { "http://www.rubyonrails.org" }
+ url_helper(:linkable) { |linkable| [:"#{linkable.linkable_type}", { id: linkable.id }] }
+ url_helper(:params) { |params| params }
+ url_helper(:symbol) { :basket }
+ url_helper(:hash) { { controller: "basket", action: "show" } }
+ url_helper(:array) { [:admin, :dashboard] }
+ url_helper(:options) { |options| [:products, options] }
+ url_helper(:defaults, size: 10) { |options| [:products, options] }
+ end
+
+ APP = build_app Routes
+
+ def app
+ APP
+ end
+
+ include Routes.url_helpers
+
+ def setup
+ @category = Category.new("1")
+ @collection = Collection.new("2")
+ @product = Product.new("3")
+ @path_params = { "controller" => "pages", "action" => "index" }
+ @unsafe_params = ActionController::Parameters.new(@path_params)
+ @safe_params = ActionController::Parameters.new(@path_params).permit(:controller, :action)
+ end
+
+ def test_custom_path_helper
+ assert_equal "http://www.rubyonrails.org", website_path
+ assert_equal "http://www.rubyonrails.org", Routes.url_helpers.website_path
+
+ assert_equal "/categories/1", linkable_path(@category)
+ assert_equal "/categories/1", Routes.url_helpers.linkable_path(@category)
+ assert_equal "/collections/2", linkable_path(@collection)
+ assert_equal "/collections/2", Routes.url_helpers.linkable_path(@collection)
+ assert_equal "/products/3", linkable_path(@product)
+ assert_equal "/products/3", Routes.url_helpers.linkable_path(@product)
+
+ assert_equal "/", params_path(@safe_params)
+ assert_equal "/", Routes.url_helpers.params_path(@safe_params)
+ assert_raises(ArgumentError) { params_path(@unsafe_params) }
+ assert_raises(ArgumentError) { Routes.url_helpers.params_path(@unsafe_params) }
+
+ assert_equal "/basket", symbol_path
+ assert_equal "/basket", Routes.url_helpers.symbol_path
+ assert_equal "/basket", hash_path
+ assert_equal "/basket", Routes.url_helpers.hash_path
+ assert_equal "/admin/dashboard", array_path
+ assert_equal "/admin/dashboard", Routes.url_helpers.array_path
+
+ assert_equal "/products?page=2", options_path(page: 2)
+ assert_equal "/products?page=2", Routes.url_helpers.options_path(page: 2)
+ assert_equal "/products?size=10", defaults_path
+ 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)
+ end
+
+ def test_custom_url_helper
+ assert_equal "http://www.rubyonrails.org", website_url
+ assert_equal "http://www.rubyonrails.org", Routes.url_helpers.website_url
+
+ assert_equal "http://www.example.com/categories/1", linkable_url(@category)
+ assert_equal "http://www.example.com/categories/1", Routes.url_helpers.linkable_url(@category)
+ assert_equal "http://www.example.com/collections/2", linkable_url(@collection)
+ assert_equal "http://www.example.com/collections/2", Routes.url_helpers.linkable_url(@collection)
+ 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/", params_url(@safe_params)
+ assert_equal "http://www.example.com/", Routes.url_helpers.params_url(@safe_params)
+ assert_raises(ArgumentError) { params_url(@unsafe_params) }
+ assert_raises(ArgumentError) { Routes.url_helpers.params_url(@unsafe_params) }
+
+ assert_equal "http://www.example.com/basket", symbol_url
+ assert_equal "http://www.example.com/basket", Routes.url_helpers.symbol_url
+ assert_equal "http://www.example.com/basket", hash_url
+ assert_equal "http://www.example.com/basket", Routes.url_helpers.hash_url
+ assert_equal "/admin/dashboard", array_path
+ assert_equal "/admin/dashboard", Routes.url_helpers.array_path
+
+ assert_equal "http://www.example.com/products?page=2", options_url(page: 2)
+ assert_equal "http://www.example.com/products?page=2", Routes.url_helpers.options_url(page: 2)
+ assert_equal "http://www.example.com/products?size=10", defaults_url
+ 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)
+ end
+end
diff --git a/railties/test/application/routing_test.rb b/railties/test/application/routing_test.rb
index c515e2b270..00c5285caa 100644
--- a/railties/test/application/routing_test.rb
+++ b/railties/test/application/routing_test.rb
@@ -263,7 +263,10 @@ module ApplicationTests
assert_equal "WIN", last_response.body
end
- { "development" => "baz", "production" => "bar" }.each do |mode, expected|
+ {
+ "development" => ["baz", "http://www.apple.com"],
+ "production" => ["bar", "http://www.microsoft.com"]
+ }.each do |mode, (expected_action, expected_url)|
test "reloads routes when configuration is changed in #{mode}" do
controller :foo, <<-RUBY
class FooController < ApplicationController
@@ -274,12 +277,19 @@ module ApplicationTests
def baz
render plain: "baz"
end
+
+ def custom
+ render plain: custom_url
+ end
end
RUBY
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get 'foo', to: 'foo#bar'
+ get 'custom', to: 'foo#custom'
+
+ url_helper(:custom) { "http://www.microsoft.com" }
end
RUBY
@@ -288,16 +298,25 @@ module ApplicationTests
get "/foo"
assert_equal "bar", last_response.body
+ get "/custom"
+ assert_equal "http://www.microsoft.com", last_response.body
+
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get 'foo', to: 'foo#baz'
+ get 'custom', to: 'foo#custom'
+
+ url_helper(:custom) { "http://www.apple.com" }
end
RUBY
sleep 0.1
get "/foo"
- assert_equal expected, last_response.body
+ assert_equal expected_action, last_response.body
+
+ get "/custom"
+ assert_equal expected_url, last_response.body
end
end
@@ -358,6 +377,10 @@ module ApplicationTests
def index
render plain: "foo"
end
+
+ def custom
+ render text: custom_url
+ end
end
RUBY
@@ -443,16 +466,19 @@ module ApplicationTests
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get ':locale/foo', to: 'foo#index', as: 'foo'
+ url_helper(:microsoft) { 'http://www.microsoft.com' }
end
RUBY
get "/en/foo"
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
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get ':locale/bar', to: 'bar#index', as: 'foo'
+ url_helper(:apple) { 'http://www.apple.com' }
end
RUBY
@@ -464,6 +490,11 @@ module ApplicationTests
get "/en/bar"
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_raises NoMethodError do
+ assert_equal "http://www.microsoft.com", Rails.application.routes.url_helpers.microsoft_url
+ end
end
test "resource routing with irregular inflection" do