Misu
Misu

Reputation: 441

How Rails decides which named route helper to use for ActiveRecord instance?

does anyone know how it works, and above all how to override it?

I work on a Rails 5.0.0.1 project. For practical reasons we use namespaced routes like

namespace :survey do
  resources :questions
  resources :answers
end

We have Question and Answer models accordingly.

The documentation tells, that the helper link_to can be used with simply passing the ActiveRecord instance as second parameter, which would mean in my case to simple write the following in the view (haml):

= link_to 'Show question', @question

This works fine if there is no namespace but goes for the named helper method question_path even if the route is namespaced - instead of survey_question_path. The point is that I would like to decide from an ActiveRecord instance which named route helper to use.

The interesting thing is that Rails knows that the Question instance will have a named route like question_path, I just cannot figure out how.

What I have figured out this far:

I thought there was a simple method to override in the model like to_partial_path for render @question, or self.controller_path in the case of controllers to render actions from another view directory, but I cannot find it.

Any help would be appreciated.

EDIT

After some more googling I stumbled upon the following paragraph in APIdock (Relying on named routes):

Passing a record (like an Active Record) instead of a hash as the options parameter will trigger the named route for that record. The lookup will happen on the name of the class. So passing a Workshop object will attempt to use the workshop_path route. If you have a nested route, such as admin_workshop_path you’ll have to call that explicitly (it’s impossible for url_for to guess that route).

Upvotes: 1

Views: 971

Answers (2)

Thyago B. Rodrigues
Thyago B. Rodrigues

Reputation: 630

As for your questions:

  • the second parameter of link_to uses a helper method url_for, which then chooses the appropriate (or not) named route helper - but how?

If you follow the #url_for method you mentioned around the source, we'll be thrown around, from ActionView::RoutingUrlFor to ActionDispatch::Routing::PolymorphicRoutes

(source: https://github.com/rails/rails/blob/870dde4710f1492c83233620f343ec414a07a950/actionview/lib/action_view/routing_url_for.rb#L115).

In the ActionDispatch::Routing::PolymorphicRoutes#polymorphic_url method, it makes use of an internal class and calls HelperMethodBuilder.polymorphic_method, passing all given options (including your model class as 'record_or_hash_or_array' (yeah, I know...)).

A few lines below, on this method's declaration, you'll get thrown around small methods a bit more and end up on a method like this:

def self.build(action, type)
  prefix = action ? "#{action}_" : ""
  suffix = type
  if action.to_s == "new"
    HelperMethodBuilder.singular prefix, suffix
  else
    HelperMethodBuilder.plural prefix, suffix
  end
end

def self.singular(prefix, suffix)
  new(->(name) { name.singular_route_key }, prefix, suffix)
end

def self.plural(prefix, suffix)
  new(->(name) { name.route_key }, prefix, suffix)
end

(source: https://github.com/rails/rails/blob/3f2b7d60a52ffb2ad2d4fcf889c06b631db1946b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb)

As you can see, it'll decide if it's the #new action or not, and call either #singular or #plural, which will, in turn, answer your second question:

  • the ActiveRecord instances have the method model_name, which contains attributes like route_key (plural) and singular_route_key - I am not sure if this has anything to do with what I am looking for, or even if it has how to override it.

These two methods get assigned, as we can see above, to a lambda that is the first parameter to this inner class's initializer, which will be stored as @key_strategy. Back in **ActionView#UrlFor", on the first link I showed you, you see this call:

else
  builder.handle_model_call(self, options)

Which will pass on to a few methods on the Builder's side:

def handle_model(record)
  args  = []

  model = record.to_model
  named_route = if model.persisted?
    args << model
    get_method_for_string model.model_name.singular_route_key
  else
    get_method_for_class model
  end

  [named_route, args]
end

def handle_model_call(target, model)
  method, args = handle_model model
  target.send(method, *args)
end

private

def get_method_for_class(klass)
  name = @key_strategy.call klass.model_name
  get_method_for_string name
end

def get_method_for_string(str)
  "#{prefix}#{str}_#{suffix}"
end

And, if I haven't missed anything, this last method returns your URL path.

Upvotes: 1

user229044
user229044

Reputation: 239402

Rails uses your model's model_name, that's it, and you shouldn't override that method. If you want to turn a Question into a call to survey_question_path, then you need to define your own question_path which returns survey_question_path...

def question_path(question)
  survey_question_path(question)
end

... or just use the full method name yourself.

You can also use arrays of symbols/models, which get passed directly to url_for:

= link_to 'Show question', [:survey, @question]

Upvotes: 2

Related Questions