I have a bunch of rails models, let's say one of them is called Post. I need a way to programmatically extract all route helpers with their respective method for a specific model.
Something like this
Rails.application.routes.named_routes.helper_names.select do |n|
n.include? "post"
end
obviously won't work, because I could have another model called OtherPost.
I would like to have something like
{
{"method": "GET", "helper": "post_path", "action": "show"},
{"method": "POST", "helper": "edit_post_path", "action": "update"},
{"method": "GET", "helper": "posts_path", "action": "index"},
...
}
I know that the action and method are in Rails.application.routes.routes
-> route.verb
and route.defaults[:action]
, but I can't seem to be able to reliably tie them to the helper name.
A route helper is a method that just produces a path. It is not concerned with the HTTP verb. So what you're looking to list is not the helpers, it's the actual routes.
Rails derives the names of the routing helpers dynamically from models through the ActiveModel::Naming API.
irb(main):005:0> Post.model_name.route_key
=> "posts"
irb(main):006:0> Post.model_name.singular_route_key
=> "post"
irb(main):007:0> OtherPost.model_name.route_key
=> "other_posts"
irb(main):008:0> OtherPost.model_name.singular_route_key
=> "other_post"
However routes in Rails have no actual coupling to your models. The only thing that links the two is convention over configuration and any attempt to automatically map the two will only work as long as the routes follow those conventions. Your example output does not.
There is also no strong link between the route helpers and the route set. The helper methods are just generated for named routes when rails builds the route set.
A route in Rails is an instance of ActionDispatch::Journey::Route
. This route may be named or not and is really just a set of constraints based on the URI, http method, headers etc.
If you look at the conventional routes generated by resources :posts
you can see that only the GET routes are named.
max@maxbox ~/p/sandbox_7 (main)> rails routes -c posts
Prefix Verb URI Pattern Controller#Action
posts GET /posts(.:format) posts#index
POST /posts(.:format) posts#create
new_post GET /posts/new(.:format) posts#new
edit_post GET /posts/:id/edit(.:format) posts#edit
post GET /posts/:id(.:format) posts#show
PATCH /posts/:id(.:format) posts#update
PUT /posts/:id(.:format) posts#update
DELETE /posts/:id(.:format) posts#destroy
That's because creating uses the collection path (posts_path) and updating and destroying uses the member path (post_path). Since these paths are the same there is no sense in generating separate helpers for these routes.
So trying to look up the routes that way is fundamentially flawed.
If we can assume that the model name lines up with the controller name we could use that to filter the routes.
irb(main):008:0> post_routes = routes.filter { |r| r.defaults[:controller]&.match?('post') }
irb(main):009:0> post_routes.map { |r| { name: r.name, pattern: r.path.spec.to_s, verb: r.verb, controller: r.defaults[:controller], action: r.defaults[:action] } } =>
[{:name=>"posts", :pattern=>"/posts(.:format)", :verb=>"GET", :controller=>"posts", :action=>"index"},
{:name=>nil, :pattern=>"/posts(.:format)", :verb=>"POST", :controller=>"posts", :action=>"create"},
{:name=>"new_post", :pattern=>"/posts/new(.:format)", :verb=>"GET", :controller=>"posts", :action=>"new"},
{:name=>"edit_post", :pattern=>"/posts/:id/edit(.:format)", :verb=>"GET", :controller=>"posts", :action=>"edit"},
{:name=>"post", :pattern=>"/posts/:id(.:format)", :verb=>"GET", :controller=>"posts", :action=>"show"},
{:name=>nil, :pattern=>"/posts/:id(.:format)", :verb=>"PATCH", :controller=>"posts", :action=>"update"},
{:name=>nil, :pattern=>"/posts/:id(.:format)", :verb=>"PUT", :controller=>"posts", :action=>"update"},
{:name=>nil, :pattern=>"/posts/:id(.:format)", :verb=>"DELETE", :controller=>"posts", :action=>"destroy"}]
However again the devil is in the details as this only works when the model name lines up with the controller name. You don't have to go further than for example adding Devise to your application before this starts breaking down. Additionally you get an additional level of complexity when dealing with namespaced routes.
There is no 100% reliable way to get the routes related to a model as that link is pretty weak. All you can do is make guesses.
Also beware that models are an internal implementation detail of an application while routes are the external API your application provides to the world. While the two may be the same in simple CRUD apps this is not always the case.
If what you're looking to do is just audit the routes a much simpler solution is just rails routes -c posts
.