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