From 31dc46cb9c8aa3e05dc955ae50ec53421951b4a5 Mon Sep 17 00:00:00 2001 From: Andrew White Date: Wed, 20 Jan 2016 14:40:34 +0000 Subject: Wrap routes.url_helpers.url_for via a proxy The singleton url_for on Rails.application.routes.url_helpers isn't the same as the url_for you get when you include the module in your class as the latter has support for polymorphic style routes, etc. whereas the former accepts only a hash and is the underlying implementation defined on ActionDispatch::Routing::RouteSet. This commit changes the singleton method to call through a proxy instance so that it gets the full range of features specified in the documentation for url_for. --- .../lib/action_dispatch/routing/route_set.rb | 25 ++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) (limited to 'actionpack/lib') diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 5b873aeab7..8ccfab56cf 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -452,17 +452,34 @@ module ActionDispatch # Define url_for in the singleton level so one can do: # Rails.application.routes.url_helpers.url_for(args) - @_routes = routes + proxy_class = Class.new do + include UrlFor + include routes.named_routes.path_helpers_module + include routes.named_routes.url_helpers_module + + attr_reader :_routes + + def initialize(routes) + @_routes = routes + end + + def optimize_routes_generation? + @_routes.optimize_routes_generation? + end + end + + @_proxy = proxy_class.new(routes) + class << self def url_for(options) - @_routes.url_for(options) + @_proxy.url_for(options) end def optimize_routes_generation? - @_routes.optimize_routes_generation? + @_proxy.optimize_routes_generation? end - attr_reader :_routes + def _routes; @_proxy._routes; end def url_options; {}; end end -- cgit v1.2.3 From ce7d5fb2e6ffa9ec323510aaff51f10b15f1649a Mon Sep 17 00:00:00 2001 From: Andrew White Date: Wed, 20 Jan 2016 15:03:10 +0000 Subject: 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. --- actionpack/lib/action_dispatch/routing/mapper.rb | 41 ++++++++++++ .../lib/action_dispatch/routing/route_set.rb | 75 ++++++++++++++++++++++ 2 files changed, 116 insertions(+) (limited to 'actionpack/lib') 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 -- cgit v1.2.3 From 47a27e8950ad00654e2ba0420cefd87269e08055 Mon Sep 17 00:00:00 2001 From: Andrew White Date: Tue, 23 Feb 2016 09:59:33 +0000 Subject: Rename url_helper to direct --- actionpack/lib/action_dispatch/routing/mapper.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) (limited to 'actionpack/lib') diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 329a374d1e..474856b23a 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -2020,20 +2020,20 @@ module ActionDispatch end end - module UrlHelpers + module DirectUrls # 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 + # direct :homepage do # "http://www.rubyonrails.org" # end # - # url_helper :commentable do |model| + # direct :commentable do |model| # [ model, anchor: model.dom_id ] # end # - # url_helper :main do + # direct :main do # { controller: 'pages', action: 'index', subdomain: 'www' } # end # @@ -2049,13 +2049,13 @@ module ActionDispatch # 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| + # direct :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) + def direct(name, options = {}, &block) @set.add_url_helper(name, options, &block) end end @@ -2153,7 +2153,7 @@ module ActionDispatch include Scoping include Concerns include Resources - include UrlHelpers + include DirectUrls end end end -- cgit v1.2.3 From e96da0a7beb483ed5e8a8096ff67b09ecc5ca7a1 Mon Sep 17 00:00:00 2001 From: Andrew White Date: Mon, 20 Feb 2017 13:54:06 +0000 Subject: Only accept symbols and strings for Mapper#direct --- actionpack/lib/action_dispatch/routing/mapper.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'actionpack/lib') diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 474856b23a..fceadc7039 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -2056,7 +2056,12 @@ module ActionDispatch # NOTE: It is the url helper's responsibility to return the correct # set of options to be passed to the `url_for` call. def direct(name, options = {}, &block) - @set.add_url_helper(name, options, &block) + case name + when String, Symbol + @set.add_url_helper(name, options, &block) + else + raise ArgumentError, "The direct method only accepts a string or symbol" + end end end -- cgit v1.2.3 From 7d1e738057cead1970d8aca31310a51a59f7235f Mon Sep 17 00:00:00 2001 From: Andrew White Date: Mon, 20 Feb 2017 13:55:29 +0000 Subject: Don't allocate a hash unnecessarily --- actionpack/lib/action_dispatch/routing/mapper.rb | 2 +- actionpack/lib/action_dispatch/routing/route_set.rb | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) (limited to 'actionpack/lib') diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index fceadc7039..3756ef15a2 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -2055,7 +2055,7 @@ module ActionDispatch # # NOTE: It is the url helper's responsibility to return the correct # set of options to be passed to the `url_for` call. - def direct(name, options = {}, &block) + def direct(name, options = nil, &block) case name when String, Symbol @set.add_url_helper(name, options, &block) diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index b1f7cd30fc..923d063655 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -210,7 +210,11 @@ module ActionDispatch private def eval_block(t, args, options) - t.instance_exec(*args, defaults.merge(options), &block) + t.instance_exec(*args, merge_defaults(options), &block) + end + + def merge_defaults(options) + defaults ? defaults.merge(options) : options end end -- cgit v1.2.3 From 3bf47b018be912fc7946342315e67b2ac6c33eaf Mon Sep 17 00:00:00 2001 From: Andrew White Date: Mon, 20 Feb 2017 20:22:42 +0000 Subject: Add custom polymorphic mapping Allow the use of `direct` to specify custom mappings for polymorphic_url, e.g: resource :basket direct(class: "Basket") { [:basket] } This will then generate the following: >> link_to "Basket", @basket => Basket More importantly it will generate the correct url when used with `form_for`. Fixes #1769. --- actionpack/lib/action_dispatch/routing/mapper.rb | 64 +++++++++++-- .../action_dispatch/routing/polymorphic_routes.rb | 27 +++++- .../lib/action_dispatch/routing/route_set.rb | 106 ++++++++++++--------- 3 files changed, 141 insertions(+), 56 deletions(-) (limited to 'actionpack/lib') diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 3756ef15a2..6123a1f5f5 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -2021,8 +2021,8 @@ module ActionDispatch end module DirectUrls - # Define a custom url helper that will be added to the url helpers - # module. This allows you override and/or replace the default behavior + # Define custom routing behavior that will be added to the application's + # routes. This allows you override and/or replace the default behavior # of routing helpers, e.g: # # direct :homepage do @@ -2037,8 +2037,35 @@ module ActionDispatch # { 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: + # The above example show how to define a custom url helper but it's also + # possible to alter the behavior of `polymorphic_url` and consequently the + # behavior of `link_to` and `form_for` when passed a model instance, e.g: + # + # direct class: "Basket" do + # [:basket] + # end + # + # NOTE: This custom behavior only applies to simple polymorphic urls where + # a single model instance is passed and not more complicated forms, e.g: + # + # # config/routes.rb + # resource :profile + # namespace :admin do + # resources :users + # end + # + # direct(class: "User") { [:profile] } + # + # # app/views/application/_menu.html.erb + # link_to 'Profile', @current_user + # link_to 'Profile', [:admin, @current_user] + # + # The first `link_to` will generate '/profile' but the second will generate + # the standard polymorphic url of '/admin/users/1'. + # + # The return value from the block passed to `direct` 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' } @@ -2046,6 +2073,9 @@ module ActionDispatch # * An Active Model instance # * An Active Model class # + # NOTE: Other url helpers can be called in the block but be careful not to invoke + # your custom url helper again otherwise it will result in a stack overflow error + # # You can also specify default options that will be passed through to # your url helper definition, e.g: # @@ -2053,14 +2083,28 @@ module ActionDispatch # [ :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 direct(name, options = nil, &block) - case name + # You can pass options to a polymorphic mapping do - the arity for the block + # needs to be two as the instance is passed as the first argument, e.g: + # + # direct class: 'Basket', anchor: 'items' do |basket, options| + # [:basket, options] + # end + # + # This generates the url '/basket#items' because when the last item in an + # array passed to `polymorphic_url` is a hash then it's treated as options + # to the url helper that gets called. + # + # NOTE: The `direct` method doesn't observe the current scope in routes.rb + # and because of this it's recommended to define them outside of any blocks + # such as `namespace` or `scope`. + def direct(name_or_hash, options = nil, &block) + case name_or_hash + when Hash + @set.add_polymorphic_mapping(name_or_hash, &block) when String, Symbol - @set.add_url_helper(name, options, &block) + @set.add_url_helper(name_or_hash, options, &block) else - raise ArgumentError, "The direct method only accepts a string or symbol" + raise ArgumentError, "The direct method only accepts a hash, string or symbol" end end end diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb index 432b9bf4c1..512e23c833 100644 --- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb +++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb @@ -103,6 +103,10 @@ module ActionDispatch return polymorphic_url record, options end + if mapping = polymorphic_mapping(record_or_hash_or_array) + return mapping.call(self, [record_or_hash_or_array], options) + end + opts = options.dup action = opts.delete :action type = opts.delete(:routing_type) || :url @@ -123,6 +127,10 @@ module ActionDispatch return polymorphic_path record, options end + if mapping = polymorphic_mapping(record_or_hash_or_array) + return mapping.call(self, [record_or_hash_or_array], options, only_path: true) + end + opts = options.dup action = opts.delete :action type = :path @@ -156,6 +164,11 @@ module ActionDispatch polymorphic_path(record_or_hash, options.merge(action: action)) end + def polymorphic_mapping(record) + return false unless record.respond_to?(:to_model) + _routes.polymorphic_mappings[record.to_model.model_name.name] + end + class HelperMethodBuilder # :nodoc: CACHE = { "path" => {}, "url" => {} } @@ -255,9 +268,13 @@ module ActionDispatch [named_route, args] end - def handle_model_call(target, model) - method, args = handle_model model - target.send(method, *args) + def handle_model_call(target, record) + if mapping = polymorphic_mapping(target, record) + mapping.call(target, [record], {}, only_path: suffix == "path") + else + method, args = handle_model(record) + target.send(method, *args) + end end def handle_list(list) @@ -303,6 +320,10 @@ module ActionDispatch private + def polymorphic_mapping(target, record) + target._routes.polymorphic_mappings[record.to_model.model_name.name] + end + def get_method_for_class(klass) name = @key_strategy.call klass.model_name get_method_for_string name diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 923d063655..8bdf0d1a53 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -160,7 +160,7 @@ module ActionDispatch def add_url_helper(name, defaults, &block) @custom_helpers << name - helper = CustomUrlHelper.new(name, defaults, &block) + helper = DirectUrlHelper.new(name, defaults, &block) @path_helpers_module.module_eval do define_method(:"#{name}_path") do |*args| @@ -177,47 +177,6 @@ module ActionDispatch 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, merge_defaults(options), &block) - end - - def merge_defaults(options) - defaults ? defaults.merge(options) : options - end - end - class UrlHelper def self.create(route, options, route_name, url_strategy) if optimize_helper?(route) @@ -380,7 +339,7 @@ module ActionDispatch attr_accessor :formatter, :set, :named_routes, :default_scope, :router attr_accessor :disable_clear_and_finalize, :resources_path_names attr_accessor :default_url_options - attr_reader :env_key + attr_reader :env_key, :polymorphic_mappings alias :routes :set @@ -422,6 +381,7 @@ module ActionDispatch @set = Journey::Routes.new @router = Journey::Router.new @set @formatter = Journey::Formatter.new self + @polymorphic_mappings = {} end def eager_load! @@ -483,6 +443,7 @@ module ActionDispatch named_routes.clear set.clear formatter.clear + @polymorphic_mappings.clear @prepend.each { |blk| eval_block(blk) } end @@ -554,6 +515,14 @@ module ActionDispatch @_proxy.optimize_routes_generation? end + def polymorphic_url(record_or_hash_or_array, options = {}) + @_proxy.polymorphic_url(record_or_hash_or_array, options) + end + + def polymorphic_path(record_or_hash_or_array, options = {}) + @_proxy.polymorphic_path(record_or_hash_or_array, options) + end + def _routes; @_proxy._routes; end def url_options; {}; end end @@ -629,10 +598,61 @@ module ActionDispatch route end + def add_polymorphic_mapping(options, &block) + defaults = options.dup + klass = defaults.delete(:class) + if klass.nil? + raise ArgumentError, "Missing :class key from polymorphic mapping options" + end + + @polymorphic_mappings[klass] = DirectUrlHelper.new(klass, defaults, &block) + end + def add_url_helper(name, options, &block) named_routes.add_url_helper(name, options, &block) end + class DirectUrlHelper + 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, merge_defaults(options), &block) + end + + def merge_defaults(options) + defaults ? defaults.merge(options) : options + end + end + class Generator PARAMETERIZE = lambda do |name, value| if name == :controller -- cgit v1.2.3 From 80dcfd014b27e560f5c4b07ee5ffa98894d8ff63 Mon Sep 17 00:00:00 2001 From: Andrew White Date: Tue, 21 Feb 2017 08:30:36 +0000 Subject: Raise an error if `direct` is inside a scope block --- actionpack/lib/action_dispatch/routing/mapper.rb | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) (limited to 'actionpack/lib') diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 6123a1f5f5..4fc76e8591 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -2094,10 +2094,13 @@ module ActionDispatch # array passed to `polymorphic_url` is a hash then it's treated as options # to the url helper that gets called. # - # NOTE: The `direct` method doesn't observe the current scope in routes.rb - # and because of this it's recommended to define them outside of any blocks - # such as `namespace` or `scope`. + # NOTE: The `direct` methodn can't be used inside of a scope block such as + # `namespace` or `scope` and will raise an error if it detects that it is. def direct(name_or_hash, options = nil, &block) + unless @scope.root? + raise RuntimeError, "The direct method can't be used inside a routes scope block" + end + case name_or_hash when Hash @set.add_polymorphic_mapping(name_or_hash, &block) @@ -2129,6 +2132,14 @@ module ActionDispatch scope_level == :nested end + def null? + @hash.nil? && @parent.nil? + end + + def root? + @parent.null? + end + def resources? scope_level == :resources end -- cgit v1.2.3 From 81a6761af2b20183c78853caa4daea4ccf6b4cb7 Mon Sep 17 00:00:00 2001 From: Andrew White Date: Tue, 21 Feb 2017 08:49:15 +0000 Subject: Support mapping of non-model classes --- .../lib/action_dispatch/routing/polymorphic_routes.rb | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) (limited to 'actionpack/lib') diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb index 512e23c833..1c44d25ef6 100644 --- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb +++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb @@ -165,8 +165,11 @@ module ActionDispatch end def polymorphic_mapping(record) - return false unless record.respond_to?(:to_model) - _routes.polymorphic_mappings[record.to_model.model_name.name] + if record.respond_to?(:to_model) + _routes.polymorphic_mappings[record.to_model.model_name.name] + else + _routes.polymorphic_mappings[record.class.name] + end end class HelperMethodBuilder # :nodoc: @@ -321,7 +324,11 @@ module ActionDispatch private def polymorphic_mapping(target, record) - target._routes.polymorphic_mappings[record.to_model.model_name.name] + if record.respond_to?(:to_model) + target._routes.polymorphic_mappings[record.to_model.model_name.name] + else + target._routes.polymorphic_mappings[record.class.name] + end end def get_method_for_class(klass) -- cgit v1.2.3 From c116eaf2217abbc83ad76ac09c4fb89e033e1cdd Mon Sep 17 00:00:00 2001 From: Andrew White Date: Tue, 21 Feb 2017 12:49:25 +0000 Subject: Prefer remove_method over undef_method Using `undef_method` means that when a route is removed any other implementations of that method in the ancestor chain are inaccessible so instead use `remove_method` which restores access to the ancestor. --- actionpack/lib/action_dispatch/routing/route_set.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'actionpack/lib') diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 8bdf0d1a53..7cab421887 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -89,11 +89,11 @@ module ActionDispatch def clear! @path_helpers.each do |helper| - @path_helpers_module.send :undef_method, helper + @path_helpers_module.send :remove_method, helper end @url_helpers.each do |helper| - @url_helpers_module.send :undef_method, helper + @url_helpers_module.send :remove_method, helper end @custom_helpers.each do |helper| @@ -101,11 +101,11 @@ module ActionDispatch url_name = :"#{helper}_url" if @path_helpers_module.method_defined?(path_name) - @path_helpers_module.send :undef_method, path_name + @path_helpers_module.send :remove_method, path_name end if @url_helpers_module.method_defined?(url_name) - @url_helpers_module.send :undef_method, url_name + @url_helpers_module.send :remove_method, url_name end end -- cgit v1.2.3 From ef862c04e5cc4deed04e0ffc70af88431803efe6 Mon Sep 17 00:00:00 2001 From: Andrew White Date: Tue, 21 Feb 2017 12:50:41 +0000 Subject: Fix typo in exception message --- actionpack/lib/action_dispatch/routing/route_set.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'actionpack/lib') diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 7cab421887..832f6f12ab 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -633,7 +633,7 @@ module ActionDispatch 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!" + raise ArgumentError, "Generating a URL from non sanitized request parameters is insecure!" end when Array opts = url_options.extract_options! -- cgit v1.2.3 From fbda6b9837e3ce41dc59de0e791c972ba6d49ba3 Mon Sep 17 00:00:00 2001 From: Andrew White Date: Tue, 21 Feb 2017 12:57:08 +0000 Subject: Push option extract into call method --- actionpack/lib/action_dispatch/routing/polymorphic_routes.rb | 6 +++--- actionpack/lib/action_dispatch/routing/route_set.rb | 9 ++++----- 2 files changed, 7 insertions(+), 8 deletions(-) (limited to 'actionpack/lib') diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb index 1c44d25ef6..984ded1ff5 100644 --- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb +++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb @@ -104,7 +104,7 @@ module ActionDispatch 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]) end opts = options.dup @@ -128,7 +128,7 @@ module ActionDispatch 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], only_path: true) end opts = options.dup @@ -273,7 +273,7 @@ module ActionDispatch def handle_model_call(target, record) if mapping = polymorphic_mapping(target, record) - mapping.call(target, [record], {}, only_path: suffix == "path") + mapping.call(target, [record], only_path: suffix == "path") else method, args = handle_model(record) target.send(method, *args) diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 832f6f12ab..a2e5351697 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -164,15 +164,13 @@ module ActionDispatch @path_helpers_module.module_eval do define_method(:"#{name}_path") do |*args| - options = args.extract_options! - helper.call(self, args, options, only_path: true) + helper.call(self, args, 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) + helper.call(self, args) end end end @@ -621,7 +619,8 @@ module ActionDispatch @block = block end - def call(t, args, options, outer_options = {}) + def call(t, args, outer_options = {}) + options = args.extract_options! url_options = eval_block(t, args, options) case url_options -- cgit v1.2.3 From d7c1e62c2cd2969b991bc4a1150b02b27f6d6e3f Mon Sep 17 00:00:00 2001 From: Andrew White Date: Tue, 21 Feb 2017 15:29:10 +0000 Subject: Split direct method into two Use a separate method called `resolve` for the custom polymorphic mapping to clarify the API. --- actionpack/lib/action_dispatch/routing/mapper.rb | 91 ++++++++++++---------- .../lib/action_dispatch/routing/route_set.rb | 14 +--- 2 files changed, 56 insertions(+), 49 deletions(-) (limited to 'actionpack/lib') diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 4fc76e8591..8b4ce1ed6a 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -2020,8 +2020,8 @@ module ActionDispatch end end - module DirectUrls - # Define custom routing behavior that will be added to the application's + module CustomUrls + # Define custom url helpers that will be added to the application's # routes. This allows you override and/or replace the default behavior # of routing helpers, e.g: # @@ -2037,14 +2037,49 @@ module ActionDispatch # { controller: 'pages', action: 'index', subdomain: 'www' } # end # - # The above example show how to define a custom url helper but it's also - # possible to alter the behavior of `polymorphic_url` and consequently the - # behavior of `link_to` and `form_for` when passed a model instance, e.g: + # The return value from the block passed to `direct` 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 + # + # NOTE: Other url helpers can be called in the block but be careful not to invoke + # your custom url helper again otherwise it will result in a stack overflow error + # + # You can also specify default options that will be passed through to + # your url helper definition, e.g: + # + # direct :browse, page: 1, size: 10 do |options| + # [ :products, options.merge(params.permit(:page, :size)) ] + # end + # + # NOTE: The `direct` methodn can't be used inside of a scope block such as + # `namespace` or `scope` and will raise an error if it detects that it is. + def direct(name, options = {}, &block) + unless @scope.root? + raise RuntimeError, "The direct method can't be used inside a routes scope block" + end + + @set.add_url_helper(name, options, &block) + end + + # Define custom polymorphic mappings of models to urls. This alters the + # behavior of `polymorphic_url` and consequently the behavior of + # `link_to` and `form_for` when passed a model instance, e.g: # - # direct class: "Basket" do + # resource :basket + # + # resolve "Basket" do # [:basket] # end # + # This will now generate '/basket' when a `Basket` instance is passed to + # `link_to` or `form_for` instead of the standard '/baskets/:id'. + # # NOTE: This custom behavior only applies to simple polymorphic urls where # a single model instance is passed and not more complicated forms, e.g: # @@ -2054,7 +2089,7 @@ module ActionDispatch # resources :users # end # - # direct(class: "User") { [:profile] } + # resolve("User") { [:profile] } # # # app/views/application/_menu.html.erb # link_to 'Profile', @current_user @@ -2063,27 +2098,7 @@ module ActionDispatch # The first `link_to` will generate '/profile' but the second will generate # the standard polymorphic url of '/admin/users/1'. # - # The return value from the block passed to `direct` 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 - # - # NOTE: Other url helpers can be called in the block but be careful not to invoke - # your custom url helper again otherwise it will result in a stack overflow error - # - # You can also specify default options that will be passed through to - # your url helper definition, e.g: - # - # direct :browse, page: 1, size: 10 do |options| - # [ :products, options.merge(params.permit(:page, :size)) ] - # end - # - # You can pass options to a polymorphic mapping do - the arity for the block + # You can pass options to a polymorphic mapping - the arity for the block # needs to be two as the instance is passed as the first argument, e.g: # # direct class: 'Basket', anchor: 'items' do |basket, options| @@ -2094,20 +2109,18 @@ module ActionDispatch # array passed to `polymorphic_url` is a hash then it's treated as options # to the url helper that gets called. # - # NOTE: The `direct` methodn can't be used inside of a scope block such as + # NOTE: The `resolve` methodn can't be used inside of a scope block such as # `namespace` or `scope` and will raise an error if it detects that it is. - def direct(name_or_hash, options = nil, &block) + def resolve(*args, &block) unless @scope.root? - raise RuntimeError, "The direct method can't be used inside a routes scope block" + raise RuntimeError, "The resolve method can't be used inside a routes scope block" end - case name_or_hash - when Hash - @set.add_polymorphic_mapping(name_or_hash, &block) - when String, Symbol - @set.add_url_helper(name_or_hash, options, &block) - else - raise ArgumentError, "The direct method only accepts a hash, string or symbol" + options = args.extract_options! + args = args.flatten(1) + + args.each do |klass| + @set.add_polymorphic_mapping(klass, options, &block) end end end @@ -2213,7 +2226,7 @@ module ActionDispatch include Scoping include Concerns include Resources - include DirectUrls + include CustomUrls end end end diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index a2e5351697..84457c97de 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -160,7 +160,7 @@ module ActionDispatch def add_url_helper(name, defaults, &block) @custom_helpers << name - helper = DirectUrlHelper.new(name, defaults, &block) + helper = CustomUrlHelper.new(name, defaults, &block) @path_helpers_module.module_eval do define_method(:"#{name}_path") do |*args| @@ -596,21 +596,15 @@ module ActionDispatch route end - def add_polymorphic_mapping(options, &block) - defaults = options.dup - klass = defaults.delete(:class) - if klass.nil? - raise ArgumentError, "Missing :class key from polymorphic mapping options" - end - - @polymorphic_mappings[klass] = DirectUrlHelper.new(klass, defaults, &block) + def add_polymorphic_mapping(klass, options, &block) + @polymorphic_mappings[klass] = CustomUrlHelper.new(klass, options, &block) end def add_url_helper(name, options, &block) named_routes.add_url_helper(name, options, &block) end - class DirectUrlHelper + class CustomUrlHelper attr_reader :name, :defaults, :block def initialize(name, defaults, &block) -- cgit v1.2.3