1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

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.
This commit is contained in:
Andrew White 2016-01-20 15:03:10 +00:00 committed by Andrew White
parent 31dc46cb9c
commit ce7d5fb2e6
4 changed files with 270 additions and 2 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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