Providing Different Formats In Merb

May 28, 2008 | Author: hassox | Publisher: hassox

Merb provides a very powerful api for responding to requests for different formats. This api is very flexible and makes responding with various formats simple.

The web is all about resources right? Information Highway and all that stuff. A lot of times we need to provide access to the resources on our site in various formats. XML, JSON, pdf, html, csv, txt and on and on. It’s really helpful to be able to do this, especially when your exposing things for an api, or using AJAX or some other technique. Responding with a different format from the same action seriously cuts down on the amount of code you need.

If your from a Rails background this concept should be nothing new. You’re likely familiar with controller actions like this.

class PostsController < Application def index @posts = Post.find(:all) respond_to do |format| format.html format.xml{ render :xml => @posts.to_xml } format.json{ render :json => @posts.to_json } end end end

The respond_to block is very flexible. It allows you to serialize your data and respond to any format you like.

Merb solves responding to different formats a little differently. Merb uses the provides api to respond to a request. Here’s the Merb equivalent of the above example.

class Posts < Application provides :xml, :json, :yaml def index @posts = Post.find(:all) display @posts end end

This example will respond to /posts with html, xml, json and yaml. Not only that, but all other actions on the controller can also respond to those formats. You can specify which formats to provide either class wide, or action specific.

Lets go through it a bit.

What’s Special About a Request

When a request comes into Merb, Merb takes a look at it so that it can setup the format that it should respond with. There’s two main ways to set this in a request.

  • Set the extension. e.g. /posts.xml
  • Set an accept header

By setting the extension, you’re explicitly telling merb what you want. Merb will try and return this format at any cost. A 404 if that format is not supported.

If you use the accept header, the first mime type that is defined in Merb that has a matching header is chosen. So if your accept header has “text/xml” in it, you should get xml back.

Using The Class API

The easiest way to use provides is with the class level api. It simply applies every relevant format to every available action.

By default, all actions provide :html

There’s a number of methods to help make this not only easy, but flexible.

  • provides – provides each of the given formats + :html
  • only_provides – only provides the supplied formats. It does not include the default of :html or any other previously set formats.
  • does_not_provide – turns off responses for this format
  • clear_provides removes all response formats. You won’t get very far in your app if you use this in a controller. But it can be a handy spec’ing tool
  • reset_provides – resets the provided formats to the defaults. :html

Each of these methods are called with a list of symbols representing the formats to operate on.

As an example provides :xml, :json, :yamlThis will give all actions in your controller the ability to respond to requests with these formats.

Using The Action API

The action level api uses the same methods as the class one, but makes it action specific. The action level provides calls are used first. If no action level calls are applicable to the request, then the class level ones are used as a fallback. This is really useful if say only one or two methods provide xml in a controller, or if you’ve got some special format that only one or two actions respond to.

Lets take a look class Posts < Application provides :xml, :json, :yaml def index @posts = Post.find(:all) display @posts end def show provides :pdf @post = Post.find(params[:id]) display @post end def new only_provides :html @post = Post.new render end end

Well… There’s a lot going on there. Lets go through it an action at a time.

The index action just responds to all the class level provided formats.

The show action responds to all the class level provided formats, and in addition responds to the :pdf format.

The new action responds to only the html format. This makes sense in this case since the new action is really only a hack for browsers to know what to submit back to the server to create a resource.

We didn’t look at the does_not_provide method, but it should be pretty self explanatory.

Render vs Display Methods

Ok I didn’t mention it before, but in the last example, you can see some methods finishing with render and some with display. In merb, there is no implicit render called on an action. Whatever is returned by the action is rendered in the browser.

The render method basically goes and looks for a template matching the template name and required format. This is the default behavior but you can change it if you need to. So, these template names will help you to respond to different formats.

index.html.haml index.xml.haml index.yaml.erb index.json.erb

So long as there is a template for the given action with the correct format it will be rendered.

The display method does what the render method does if there’s a template there, but with one main addition. You pass an object to it to be serialized if there is no template with the right format. So if the request is for :yaml format an you only had this template:

show.html.haml

The passed in object, @post would have to_yaml called on it to serialize it for you. It’s the same with the other formats. to_xml, to_json and whatever you set the to_pdf method as.

You can control what happens with the display serialization a bit by passing in options. There’s a couple of reserved options :template and :layout but any other options are passed on through to the serialization method.

display @posts, :layout => false, :methods => [:owner_name, :owner_id], :except => [:id, :filename, :updated_at, :user_id], :skip_types => true

Lets say this is for an :xml call. This would send everything to but :layout to the to_xml method. Neat.

The display method is just begging for some presenter pattern love.

Get Flexible

The render and display methods, while convenient aren’t all that flexible really. You can still have all the flexibility ruby affords with a simple case statement. Merb has a content_type method that you can check whenever you like in your action to determine what to do for a specific format if you need to. Plain old ruby case and if / else are you friends here. Lets take a look.

class Post < Application provides :xml, :yaml, :json def index provides :jpg @posts = Post.find(:all) case content_type when :jpg send_data image_data, :disposition => :inline else display @posts end end end

What Formats does Merb Provide?

The Merb::BootLoader::MimeTypes boot loader currently sets up formats for :yaml, :text, :html, :xml, :js and :json.

Declare Your Own Formats

You can declare your own formats too. With the Merb.add_mime_type method. This method lest you specify your own formats pretty easily. Here’s how to specify the :pdf format from before.

Merb.add_mime_type(:pdf, :to_pdf, %w[application/pdf], "Content-Encoding" => "gzip")

:pdf is two things. 1. the label for the format, and 2. the extension to use in the url to activate this format.

:to_pdf is the serialization method to call on the object. This could be anything you like. Merb will call this method on the passed in object.

The 3rd argument is an array of accept headers that is applicable to this format. The first one listed will be returned to the client as the response content type header.

The last, and optional argument, is any additional HTTP header’s to set whenever this format is rendered.

Really Custom Formats

You can set the format any time you like explicitly in your controller with content_type = :my_format

This could be handy to setup a format for the iPhone. That way your application can just respond to the iPhone format just like any other. You can put something like this into your application to get the iPhone format.

In config/init.rb Merb::BootLoader.after_app_loads do # iPhone mime-type Merb.add_mime_type(:iphone, :to_html, %w[application/xhtml+xml]) end In app/controllers/application.rb class Application < Merb::Controller before :set_iphone_content_type private def set_iphone_content_type self.content_type = :html if self.content_type == :iphone self.content_type = :iphone if request.user_agent =~ /(Mobile\/.+Safari)/ end end

This might look a little strange at first, with all the setting of the content type. Basically, on some browsers, the application/xhtml+xml is set as an accept header for html so the iPhone format is rendered by mistake for html. By checking to see if it’s set to :iphone first, we can set this back to :html. Then we just set it to :iphone if the user_agent is right.

No need to setup separate actions to deal with the iPhone. Just render the data as a different format. Call you view template something like:

show.iphone.haml

Comments

Sign in to make your voice heard