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.

 1def index
 2  @articles = Article.find(:all)
 3  respond_to do |format|
 4    format.html
 5    format.xml { render :xml => @articles.to_xml }
 6  end
 7end
 8
 9def show
10  @article = Article.find(params[:id])
11  respond_to do |format|
12    format.html
13    format.xml { render :xml => @article.to_xml }
14  end
15end

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?

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

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)

 1class << self
 2  def wrap_respond_to_in(*actions)
 3    if actions.include?(:all)
 4      # Also remove #rescue_action because this gets set in tests. Also include some of my friendly #find methods.
 5      actions = actions + self.public_instance_methods - ApplicationController.public_instance_methods - ['rescue_action'] - [:all]
 6      [:find_collection, :find_new, :find_member].each do |finder|
 7        actions << finder if method_defined?(finder)
 8      end
 9    end
10
11    # Returns the appropriate Responder format based on the action.
12    define_method(:format) do
13      @format.last rescue nil
14    end
15
16    for action in actions
17      action = action.to_sym
18
19      module_eval <<-END
20        alias_method(:__#{action.to_i}__, :#{action.to_s})
21        private :__#{action.to_i}__
22
23        def #{action.to_s}(*args)
24          result = nil      # This is the result of whatever method this wraps
25          respond_to do |format|
26            @format ||= []
27            @format.push(format)
28            result = __#{action.to_i}__(*args)
29            @format.pop
30          end
31          return result
32        end
33      END
34    end
35  end
36end

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:

 1module ActionController #:nodoc:
 2  module MimeResponds #:nodoc:
 3    class Responder
 4      alias_method :__respond_without_check_for_empty_order__, :respond
 5      private :__respond_without_check_for_empty_order__
 6
 7      def respond
 8        return if @order.empty?
 9        __respond_and_check_for_empty_order__
10      end
11    end
12  end
13end

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.

 1class Articles < ApplicationController
 2  before_filter :find_member, :only => [:show]
 3
 4  def show
 5    format.html
 6    format.xml { render :xml => @article.to_xml }
 7  end
 8
 9  protected
10    def find_member
11      @article = Article.find(params[:id])
12    rescue ActiveRecord::RecordNotFound
13      format.html do
14        flash[:error] = "Article was not found"
15        redirect_to articles_url
16      end
17      format.xml { head :status => 404 }
18    end
19
20    wrap_respond_to_in :all
21end

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