2

I need to override quite a few path helper methods in Ruby on Rails and call super from them. My standard approach would be:
path_helper.rb

def business_path(business)
  if params[:city_id] == 2
    moscow_business_path(business)
  else
    super
  end
end

But I have a lot of these methods, so I want to define them dynamically like this:

  %i[root businesses business ...].each do |path_method|
    method_name = "#{path_method}_path"
    old_method = method(method_name)
    define_method(method_name) do |*args|
      if params[:city_id] == 2
        public_send("moscow_#{method_name}")
      else
        old_method.call(*args)
      end
    end
  end

But I get this error:

 /home/leemour/Ruby/burobiz/app/helpers/path_helper.rb:31:in `method': undefined method `root_path' for class `Module' (NameError) 
 from /home/leemour/Ruby/burobiz/app/helpers/path_helper.rb:31:in `block in <module:PathHelper>'
 from /home/leemour/Ruby/burobiz/app/helpers/path_helper.rb:29:in `each' 
 from /home/leemour/Ruby/burobiz/app/helpers/path_helper.rb:29:in `<module:PathHelper>' 
 from /home/leemour/Ruby/burobiz/app/helpers/path_helper.rb:1:in `<top (required)>' 
 from /home/leemour/.rbenv/versions/2.3.3/lib/ruby/gems/2.3.0/gems/activesupport-5.1.3/lib/active_support/dependencies.rb:476:in `load'

I guess helper modules haven't been included yet, so there is no original path helper method to capture with method(method_name). Then I guess I'd have to use self.included hook but I couldn't figure it. How can I adjust this code to make it work? (I don't want to use eval).

leemour
  • 11,414
  • 7
  • 36
  • 43

2 Answers2

3

Instead of trying to use Kernel.method, have you tried using the Rails.application.routes.url_helpers method?

%i[...].each do |path_method|
  define_method("#{path_method}_path") do |*args|
    if params[:city_id] == 2
      public_send("moscow_#{path_method}_path", *args)
    else
      Rails.application.routes.url_helpers.public_send("#{path_method}_path", *arsg)
    end
  end
end
Aaron Breckenridge
  • 1,723
  • 18
  • 25
  • Thanks, that works. However, I'd also like to know how to define methods after the module is included so instead of a hack with `url_helpers` I'd have a common solution. Could you help? – leemour May 23 '18 at 20:18
  • 1
    Use `public_send` with url_helpers too, for consistency. – Sergio Tulentsev May 23 '18 at 21:29
  • @leemour: FWIW, I consider _your_ way to be the hacky one here. ¯\\_(ツ)_/¯ Usage of url_helpers is common outside of controller context (say models or something like that). – Sergio Tulentsev May 23 '18 at 21:32
  • @leemour: I myself haven't tried this, but I have a good feeling that if you monkeypatch the url helpers object (in an initializer or something), you'll achieve what you want. – Sergio Tulentsev May 23 '18 at 21:38
  • 1
    Also send the args too as right now they are being dropped so things like `business_path(@business)` will fail. – engineersmnky May 23 '18 at 23:43
2

You could just wrap all your calls too

def path_name_for(path_name,*args) 
  path = "#{path_name}_path"
  path.prepend("moscow_") if params[:city] == 2
  public_send(path,*args)
end 

Then in your views simply call

<%= link_to 'Business Path', path_name_for(:business, @business) %>

This makes the routing a bit clearer to me as it makes it a bit more obvious that there is a custom implementation rather than overriding a known implementation.

Also this too might be a possibility (Although untested this should function like your existing code in "path_helper.rb")

module PathHelper
  module MoscowRedirect
    def self.prepended(base) 
      %i[...].each do |path_name|
        define_method("#{path_name}_path") do |*args|
          params[:city] == 2 ? public_send("moscow_#{__method__}",*args) : super(*args)
        end
      end
    end
  end
  self.prepend(MoscowRedirect)
end 
engineersmnky
  • 25,495
  • 2
  • 36
  • 52