Today I show how to easily solve an incredibly annoying (but small) problem in Rails Routing for Resources.
For my past few Rails projects, I've created a module called ModelLoader that I include in my Controllers to load requested Models from the database. For each of my models, I have a #load_xxx
and a require_xxx!
. For an app that contains People and Tickets, the model loader would look something like this:
module Utilities
module Controller
module ModelLoader
def load_person
safe_load :@person, Person, :person_id
end
def require_person!
require_exists! :@person, Person, :person_id
end
def load_ticket
safe_load :@ticket, Ticket, :ticket_id
end
def require_ticket!
require_exists! :@ticket, Ticket, :ticket_id
end
private
def safe_load(variable_name, klass, parameter_name)
begin
instance_variable_set(variable_name, klass.find(params[parameter_name]))
rescue ActiveRecord::RecordNotFound => e
nil #swallow it; must have a statement here for coverage to see the line
end
end
def require_exists!(variable_name, klass, parameter_name)
raise error_for(klass, parameter_name) unless instance_variable_get(variable_name)
end
def error_for(klass, parameter_name)
if params[parameter_name]
msg = "Could not find #{klass} with id #{params[parameter_name]}"
else
msg = "Parameter #{parameter_name} is required"
end
ActiveRecord::RecordNotFound.new(msg)
end
end
end
end
In my Controllers, I just do something like
append_before_filter :load_person, :only => [:foo, :bar]
This is almost perfect. The problem is that some of my actions are accessible via more than one route. In particular, a nested- and non-nested version of the same resource:
map.resources :people do |people|
people.resources :tickets
end
map.resources :tickets
gives routes like
/people/:id/edit
/people/:person_id/tickets/:id
/tickets/:id
That means that somestimes :id is a Person#id and sometimes it's a Ticket#id. This wreaks havoc on my model loader. (It's also a problem for Sutto's similar, but more elegant solution.)
The solution is simple:
module ActionController
module Resources
class Resource
def member_path
@member_path ||= "#{path}/:#{singular}_id"
end
end
end
end
Now all route segments have the class name in them:
/people/:person_id/edit
/people/:person_id/tickets/:ticket_id
/tickets/:ticket_id