Sunday, June 22, 2008

RESTful Rails: param_parsers and XML issue

Rails, as a framework, and REST, as an architecture, have always been bound since the first appearance of Rails. Rails seamlessly promotes REST through a set of awesome features, including RESTful routing, resource url helpers, respond_to blocks, and others.

One of the most handy features that promotes the use of REST in Rails is ActionController::Base.param_parsers. param_parsers is a hash that maps standard (or even user-defined) MIME types to a parser procedure. The use of such procedures appears when the 'content-type' header of the request is set to one of the known MIME type names. In such a case, the parser is automatically invoked to parse the request body and evaluate the famous 'params' hash from it.

For example, "text/xml" and "application/xml" content-types both map to 'XML' MIME type. If a request is issued with such a content-type header, the procedure ActionController::Base.param_parsers[Mime::XML] is automatically invoked, evaluating params from the XML request body. This is really magnificent when you're writing a uniform RESTful application that's wanted to be able to comprehend, and respond with, multiple formats. You just write your action logic depending on the presence of the 'params' hash, letting the param_parsers to handle the translation.

However, there is a fundamental problem when talking XML. In a typical case, one of your controller actions could be waiting for more than one root param. The fundamental problem with XML is that it could only have a single root element. This means that, when posting XML, you can only post one root parameter. For example, the action logic could be operating on two parameters: params[:name] and params [:email]. With no single parent to both, XML form of this params hash can never be formed. And, of course, we don't want to change our application logic to overcome this problem. we need to attack it in its heart, the parsing part.

The good news is, it's hackable !! Rails lets you define a custom MIME type and its param_parser, or even override an existing MIME type param_parser. All we need to do is to define a conventional wrapper to be used in such a case. For example, we could announce that, in all cases of sending multiple parameters in XML, a single parent called 'hash' (or anything else) should wrap all of them. Then we override the XML param_parser to extract the wrapped tags to params.
ActionController::Base.param_parsers[Mime::XML] = Proc.new do |data|
source = Hash.from_xml data
result = {}
if source.keys.length == 1 and source.keys[0] == "hash"
source['api_hash'].each { |k, v| result[k.to_sym] = v }
result
else
source
end
end

All we have done is that we formed the hash from the posted XML (data), then checking for the very case of "a single root named hash", extracting its inner values to the root. Like this, our application logic needn't be touched.

No comments: