diff options
-rw-r--r-- | actionpack/lib/action_dispatch/routing/mapper.rb | 41 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/routing/route_set.rb | 75 | ||||
-rw-r--r-- | actionpack/test/dispatch/routing/custom_url_helpers_test.rb | 121 | ||||
-rw-r--r-- | railties/test/application/routing_test.rb | 35 |
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 |