dmathieu - Render your pages differently with responders

Render your pages differently with responders

Monday, 28 March 2011 in Development Articles by Damien Mathieu Creative Commons License

You probably remember doing the following in Rails 2 :

1
2
3
4
respond_to do |format|
    format.html
    format.json { render :json => @object }
end

In rails 3, you can replace those calls with :

1
respond_with @object

The first solution is still working of course. The idea with respond_with is that in 99% of the cases, you’ll do the same thing, depending of the format. In HTML, we render the content of a template. In any other format, we render the parsed content (in json or xml for example).

However, you might need to do some different things.
Let’s guess, for example, that you wish to render your page in iCal.
With respond_to, you’d just add the format and define inside the blog what needs to be done.

With respond_with, it’s not possible anymore as you don’t have direct control of what’s happening.
However, of course, you can use responders which will allow us to do what we expect to get.

responder, what’s that ?

As you can guess, when you use respond_with, rails knows what do to with your object, depending of the request’s format.
The responder is the component which knows what to do.

In order to render our application differently, we’ll just have to create our very own responder.
Let’s look at the internal rails’ responder :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
module ActionController
    class Responder
        attr_reader :controller, :request, :format, :resource, :resources, :options

        def initialize(controller, resources, options={})
            @controller = controller
            @request = @controller.request
            @format = @controller.formats.first
            @resource = resources.last
            @resources = resources
            @options = options
            @action = options.delete(:action)
            @default_response = options.delete(:default_response)
        end

        def self.call(*args)
            new(*args).respond
        end

        def respond
            method = "to_#{format}"
            respond_to?(method) ? send(method) : to_format
        end

       def to_html
           default_render
        rescue ActionView::MissingTemplate => e
            navigation_behavior(e)
        end

        def to_format
            default_render
        rescue ActionView::MissingTemplate => e
            api_behavior(e)
        end
    end
end

I only kept what we need for the purpose of this article. You can find the entire file at action_controller/metal/responder.rb.

Our responder will be called via it’s call method, which will initialize the object and call it’s method respond.
If you wich to create your own responder, you just need an object which responds to self.call.

In the default responder, we can see, in the respond method that, if a to_xx method is defined, we call it. That’s for HTML.
Otherwise, we use to_format.

In both cases, the default rendering will be the same. The default_render method will try to find a template in your views.
When there’s no view, the behavior or quite different.

In HTML, it render an error. We can’t display anything without a view ! In any other format, we’ll render the object in the appropriate format.

So, in the api_behavior method, we have :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def api_behavior(error)
     raise error unless resourceful?

     if get?
         display resource
     elsif has_errors?
        display resource.errors, :status => :unprocessable_entity
     elsif post?
        display resource, :status => :created, :location => api_location
     elsif has_empty_resource_definition?
        display empty_resource, :status => :ok
     else
         head :ok
     end
end

We can see here that, depending of the request’s format (get, post) and whether the object has validation errors or not, we render the content differently (and we render it with a different HTTP status code).

That’s what, in a rails 2 application, we’d have done the following way :

1
2
3
4
5
6
7
8
9
respond_do do |format|
    format.json do
        if @object.save
            render :json => @object
        else
            render :json => @object, :status => :unprocessable_entity
        end
    end
end

That kind of code was repeated in all our controllers. We don’t need to do it anymore !

Ok, that’s cool. But what can I do exactly ?

That presentation is nice. But what can we really do with responders ? In the Crafting Rails Applications book, José Valim gets into the details of responders and gives several examples.
You can find them on github : github.com/plataformatec/responders.

There is, for example, the HTTP Cache Responder. It overrides the to_format method.

1
2
3
4
def to_format
    return if do_http_cache? && do_http_cache!
    super
end

And adds two methods : do_http_cache? and do_http_cache!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def do_http_cache!
    timestamp = resources.flatten.map do |resource|
        resource.updated_at.try(:utc) if resource.respond_to?(:updated_at)
    end.compact.max

    controller.response.last_modified ||= timestamp if timestamp

    head :not_modified if fresh = request.fresh?(controller.response)
    fresh
end

def do_http_cache?
    get? && @http_cache != false && ActionController::Base.perform_caching && persisted? && resourceful?
end

We can see in the second one that we accept caching if we’re on a GET request, the cache is activated and the resource exists in the database.

In the first method, we take the last modification date and we compare it with the last_modified HTTP header from the request.
If it’s “fresh” (the object hasn’t been modified since it’s was last viewed), we can send a head :not_modified.

In 40 lignes of code, this responder implements an HTTP cache on all the non-HTML calls of your application !

In the same kind, on a “top secret” project, I wanted to be able to have different jsons for the same object, depending of the current user.
It allows me to display only some fields in the json, depending of whether the user can see the information or not.

So I wanted to know who is the current user in my model’s serializable_hash method.
This method is used to get a hash from a model and send it in any format (json, xml, …).

So I did the HashParameters responder.
With that responder, in my controller, I can now define a method serializable_hash_parameters, which return a hash.
The parameters for this hash will be provided to my model.

I’m doing it in the internal method display.

1
2
3
4
def display(resource, given_options={})
    given_options.merge!(controller.send(:serializable_hash_parameters)) if controller.respond_to?(:serializable_hash_parameters, true)
    super(resource, given_options)
end

And how can I activate my responder ?

In my HashParameters example, you can see that I define a module named “Responder”, which I include in my ApplicationController.
This module will allow me to include several responders without having to worry about updating my controller.
By doing that, I can decide to export my responders to an external gem and reuse them in an other application.

There is my Responder module :

1
2
3
4
5
6
7
8
9
10
11
12
13
module Responder
    class ClassMethods < ActionController::Responder
        include ::Responder::HashParameters
    end

    def self.included(klass)
        klass.class_eval do
            def self.responder
                Responder::ClassMethods
            end
        end
    end
end

When it’s included, the self.included method will override the self.responder method from my controller, therefor changing my application’s responder.

I set as responder the Responder::ClassMethods class, in which I include the “HashParameters” module.
As of now, my controller’s responder is the one I just configured !

Conclusion

As you have seen, responders allows us to fundamentally change the behavior of our application and helps us remain particularly DRY.

Don’t be afraid to user them ! And if you’ve not yet migrated to rails 3, this feature by itself should be a hell of an argument in favor of doing it.