ShiftEleven

DRYing Up respond_to

Now that that release candidate for rails 1.2 is out, people are going to have even more reasons to use the respond_to method in their controller methods. With that, there may come some unsettling feeling that you're just repeating yourself over and over again.

def index
  @articles = Article.find(:all)
  respond_to do |format|
    format.html
    format.xml { render :xml => @articles.to_xml }
  end
end

def show
  @article = Article.find(params[:id])
  respond_to do |format|
    format.html
    format.xml { render :xml => @article.to_xml }
  end
end

If we want to use the respond_to, we have to make sure it's in all of our methods. Wouldn't it be easier to just do something like this?

def index
  @articles = Article.find(:all)
  format.html
  format.xml { render :xml => @articles.to_xml }
end

Doing it this way, we don't have to write 2 extra lines per method, and we don't increase our indentation. While that's not a huge thing, I still just don't like to constantly write out those lines when I don't have to when I think we can build something a little trickier.

The Breakdown

In the simplified example I gave above, we have several things that we need to accomplish. One is to wrap our methods with a respond_to by using alias_method. Well, it's actually not that simple because respond_to is a block and we will need to do some trickery there. Another task is we need to create a format method. Since we won't be calling format with a block parameter, we need to make a method which will do the dirty work for us. Finally, we need to make a tweak to ActionController::MimeResponders::Respond#respond for some of the other things that we are doing.

Adding wrap_respond_to_in to our ApplicationController

To start things off, we need to make a class method, which I have dubbed wrap_respond_to_in (I would love suggestions for names here)

class << self
  def wrap_respond_to_in(*actions)
    if actions.include?(:all)
      # Also remove #rescue_action because this gets set in tests. Also include some of my friendly #find methods.
      actions = actions + self.public_instance_methods - ApplicationController.public_instance_methods - ['rescue_action'] - [:all]
      [:find_collection, :find_new, :find_member].each do |finder|
        actions << finder if method_defined?(finder)
      end
    end

    # Returns the appropriate Responder format based on the action.
    define_method(:format) do
      @format.last rescue nil
    end

    for action in actions
      action = action.to_sym

      module_eval <<-END
        alias_method(:__#{action.to_i}__, :#{action.to_s})
        private :__#{action.to_i}__

        def #{action.to_s}(*args)
          result = nil      # This is the result of whatever method this wraps
          respond_to do |format|
            @format ||= []
            @format.push(format)
            result = __#{action.to_i}__(*args)
            @format.pop
          end
          return result
        end
      END
    end
  end
end

Let's break this down by section. First things first, the method's arguments should be a list of symbols for the names of the methods we would like to use our respond_to shorthand. Because I would like to use this shorthand for all of my action methods, I have included the notion of sending :all as a parameter. This will make sure that all of the public methods and some protected methods that I use (find_member, find_collection, and find_new). Again, I like to keep things DRY(Don't Repeat Yourself). Next we define a method called format. This will be an accessor to the last element of our format instance variable array. More on this later. After we have defined that, we need to loop through all of our methods that we want to use respond_to. In each loop, we need to make an alias for our methods. This is so that we can re-write the method, yet still keep a copy of the original code. Next we overwrite the method. As you can see here, this is where we put the respond_to block. Now in this block, we have access to the format variable. Next we push this value into our format instance variable. The reasoning for using an array is to accommodate for a nested set of calls. After we push format onto format, we then call our original method, which uses the shorthand and save that to to a variable. Once that is done, we pop off the last value we put into the format@ variable, since we are done with it. Finally, return the result of our original method. This is the backbone for what we need to do. Now we need to implement it.

Tweaking ActionController::MimeResponds::Responder

Because we now have assumed that all of our methods are going to use respond_to, we need to make a way out if for some reason we don't need to use said method. This would mean that our method exists without calling the format method. In order to do that, we can use the following code:

module ActionController #:nodoc:
  module MimeResponds #:nodoc:
    class Responder
      alias_method :__respond_without_check_for_empty_order__, :respond
      private :__respond_without_check_for_empty_order__

      def respond
        return if @order.empty?
        __respond_and_check_for_empty_order__
      end
    end
  end
end

We simply do a check to see if order is empty. If so, that means our format method was never called, so we just simply need to exit out of the respond method. I put this in my lib directory and make sure that my app requires this after rails has been loaded.

Using wrap_respond_to_in

Quite simply all we need to do to use it is to call it, but we must do so after we have defined all of our methods for which we want to use this technique.

class Articles < ApplicationController
  before_filter :find_member, :only => [:show]

  def show
    format.html
    format.xml { render :xml => @article.to_xml }
  end

  protected
    def find_member
      @article = Article.find(params[:id])
    rescue ActiveRecord::RecordNotFound
      format.html do
        flash[:error] = "Article was not found"
        redirect_to articles_url
      end
      format.xml { head :status => 404 }
    end

    wrap_respond_to_in :all
end

While this is not a complete controller, it does show you how to use it.

Final Words

There's no reason to put a respond_to call in all of the methods when there is a way around it. Once I figure out how to get SVN working with Dreamhost, I am going to put this into a plugin for all to use.

Comments

comments powered by Disqus