Let’s get back to REST and Rails. One of the things Rails doesn’t support is HTTP
To accomplish this in Rails 1.0 means manually setting the content type. One place to do this is in a controller. But that doesn’t seem right. Controllers help guide a client request from its inception to its fulfillment. A controller should have no knowledge about how the results are rendered. That’s the realm of ActiveView. Now you are talking! But wait…by the time a view is invoked by Rails the content type has already been decided (that’s not quite true, but you sure don’t want to be in an RHTML template generating YAML).
How do we such a thing? Well, first go read Joe Gregorios’ excellent introduction to the subject and implementation in Python. Then download this Rails plugin that I wrote a few month ago that implements support for Mime Media Types in Ruby and integrates them into Rails.
The key bit of code is the negotiate_content method. It iterates, in order of importance as defined by the client, the different Mime Types supported by the client. For each Mime Type, it sees if there is a corresponding template with the right extension. Once it finds one, the template is loaded and the request is fulfilled:
# Copyright (c) 2006 MapBuzz # Released under the MIT License # Author: Charlie Savage module ActionView class Base def pick_template_extension(template_path)#:nodoc: if match = delegate_template_exists?(template_path) match.first else negotiate_content(template_path) end end def negotiate_content(template_path) result = 'rhtml' if !controller.respond_to?(:request) return result end negotiator = ContentNegotiator.new (controller.request.env['HTTP_ACCEPT']) # In order of preference, as specified by the client via # HTTP_ACCEPT, check to see if we can produce the media type negotiator.media_types.each do |media_type| extension = media_type.extension if media_type.extension and template_exists?(template_path, extension) result = extension # Break out of this block break end end # If all else fails return rhtml result end end end
One thing that is not immediately obvious is that this code will run every time a template or partial is run. We can use this our advantage. For example, let’s say you want to serve HTML to Internet Explorer and XHTML to Firefox.
First, in your layout file do this:
<%= render(:partial => 'layouts/doctype') %>
Next, create one partial for Internet Explorer, called _doctype.rhtml:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html> <% controller.response.headers["Content-Type"] = MediaType::HTML %>
And now one for Firefox called _doctype.rxhtml (make sure to get the extension right!):
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> <% controller.response.headers['Content-Type'] = MediaType::XHTML %>
Notice that we are neatly able to reuse 99% of our templates for both browsers, yet at the same time include the <?xml…?> header for Firefox but not Internet Explorer.
Last, its good to see that the Rails team has recently discovered the HTTP Accept header and is adding support in Rails 1.1. I haven’t had a chance to look at their implementation yet since I’m not running Edge Rails. So take the following comment with a grain of salt. Its certainly a good thing for Rails to become more aware of MimeTypes, but as mentioned above, I don’t think controllers are the right place to do this.
Once I get my hands on Rails 1.1 I’ll make sure to update this plugin if it is still useful.