aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAndrew White <andrew.white@unboxedconsulting.com>2016-01-20 15:03:10 +0000
committerAndrew White <andrew.white@unboxed.co>2017-02-21 15:30:46 +0000
commitce7d5fb2e6ffa9ec323510aaff51f10b15f1649a (patch)
treeb732091b7fc3b22edd3dabb5b4600ccd1dcb0cd1
parent31dc46cb9c8aa3e05dc955ae50ec53421951b4a5 (diff)
downloadrails-ce7d5fb2e6ffa9ec323510aaff51f10b15f1649a.tar.gz
rails-ce7d5fb2e6ffa9ec323510aaff51f10b15f1649a.tar.bz2
rails-ce7d5fb2e6ffa9ec323510aaff51f10b15f1649a.zip
Add support for defining custom url helpers in routes.rb
Allow the definition of custom url helpers that will be available automatically wherever standard url helpers are available. The current solution is to create helper methods in ApplicationHelper or some other helper module and this isn't a great solution since the url helper module can be called directly or included in another class which doesn't include the normal helper modules. Reference #22512.
-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