09 February, 2008

The Problem With Rails Resource Routes

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

3 comments:

James Golick said...

If you just use a resource plugin like resource_controller or make_resourceful, it will take care of all of this for you, and then some.

pay per head said...

Thank you for sharing to us.there are many person searching about that now they will find enough resources by your post.I would like to join your blog anyway so please continue sharing with us

price per head online said...

Here I found some interesting and useful information ... It was nice visiting your blog.