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