If you have a few moments, please read and tell me what you think would be the better option for each of these.
1. JSON says
Here’s a nice party trick:
respond_to do |format|
format.html
format.xml { render :xml=>@item }
format.json { render :json=>@item }
end
When I’m feeling particularly evil, I walk over to someone still struggling to dual-render XML and HTML using indecipherable XSLT, ask them when JSON transformation will hit the roadmap, watch them squirm, jot these few lines on a piece of paper, put it on the table and quietly walk away.
OK, not really. Actually I’m struggling doing it the other way around. Wouldn’t it make sense that I could GET a representation, change it, and PUT it back?
For XML it turns out to be quite simple:
def update Item.update params[:id], params['item'] end
Rails grabs the request body, an XML document with the document element item and stuffs the entire thing into a parameter with the same name.
If you’re lazy you get rewarded. Let Rails do all the form building for you, and its magical helpers will use field names like item[name], item[count] and item[tags][]. Those are nicely parsed into a hash called item, so the above code will also work with hForm.
But what about JSON? Test case please:
curl -d "{ name: \"foo\", count: 2, tags: [ \"bar\" ] }" -H "Content-Type: application/json"
The data is here, alright, and it’s the same structure we got back on the previous GET, but there’s nothing wrapping it up. JSON is naked. No XML document element name we can use to decide which parameter to place it in. Which means, one of these:
Option 1. All JSON objects are called such:
def update Item.update params[:id], params['item'] || params[:json] end
This works nicely as long as you remember to always param[:json], which given my propensity to easily forget, is why I’m not particularly thrilled.
Option 2. All non-named params are always called the same:
def update Item.update params[:id], params['item'] || params[:data] end
Looks the same, but is not. This option is forward looking and anticipates the possibility of some future technology we would want to use without having to go back and fix old code.
Option 3. It always is data:
def update Item.update params[:id], params[:data] end
Even XML documents get called data. Everything gets called data. Which doesn’t work for query parameters or forms, but just for completeness I had to list this as well.
Option 4. Name inference:
def update Item.update params[:id], params['item'] end
This only works because the controller is called ItemsController, and the downcased singular name is item, and so we can infer what the JSON object should be called more often than not.
I’m personally leaning towards a combination of both #1 and #4, so you can :json or :item it. What do you think?
2. Are JSON objects hashes or arrays?
All the use cases I have are for receiving records, whether represented as JSON objects or XML documents:
<item>
<name>foo</name>
</item>
{ name: "foo" }
But I do have code that renders collections, and contemplating the idea of receiving multiple records as inputs:
<items type='array'>
<item>
<name>foo</name>
</item>
</items>
[ { name: "foo" } ]
The thinking at Rails core is that hashes are enough, although there’s no such restriction on XML input. Have you considered a use case for receiving arrays (XML and/or JSON)?
3. redirect_to vs. see_other
Here’s something you see quite often in Rails apps:
def create article = Article.create(params[:article]) redirect_to article end
It seems like the right thing to do. It handles a POST request by creating the new resource, and then redirects the client to that resource (here, using polymorphic routes, a nice addition in Rails 2.o). Except it doesn’t, redirect_to will send back a 302 (Found) status code.
The 302 status code tells the client that the resource is found in a different location. By which we mean the original resource, so the proper thing to do is head over to the new location and do the POST all over again. Not what the controller author had in mind!
Per the HTTP specification, you would want to send back a 303 (See Other). Redirects (301, 302, 307) tell the client that the resource moved, and please can you try sending the same request over to the new location. 303 tells the client the request went through, got processed, please check the other location for the result.
So how come this happens and the world doesn’t fall apart? Turns out browser developers got lazy, and some handled 302 and 303 the same way. People wrote CGI scripts by brushing through the HTTP spec and just testing out what works in the browser. And copying other people’s code. Eventually this bug got codified into the Undocumented HTTP Specification. So redirect_to doesn’t break browsers.
When people write client applications that talk to a Web service, they either go lazy or go HTTP. Those that go lazy (send request, extract Location header) won’t break, but they’re losing an important HTTP feature: the ability to move resources and leave behind a forwarding address. Those that go full HTTP will either raise and error or attempt a second POST.
I think we need to fix this, and get people to write services properly from day one.Option 1. Rails (2.0) gives you two options, both of which are examples for elegant use of hashed arguments:
head :see_other, :location=>article_url(article) redirect_to article, :status=>:see_other
Both return 303, as HTTP intended it to be, so not much to complain, except we all know how likely it is that people will take the extra step to add an obscure status code that so far hoards of developers ignored.
Option 2. Temporary/permanent redirect and See Other are not the same thing, let’s make the difference known and introduce a see_other method:
# Use this in response to an HTTP POST (or PUT), telling the client where the new resource is.
# Works just like redirect_to, but sends back a 303 (See Other) status code. Redirects should be used
# to tell the client to repeat the same request on a different resource, and see_other when we want the
# client to follow a POST (on this resource) with a GET (to the new resource).
def see_other(options = {})
if options.is_a?(Hash)
redirect_to options.merge(:status=>:see_other)
else
redirect_to options, :status=>:see_other
end
end
Option 3. Or sprinkle a bit of magic on redirect_to:
def redirect_to(options = {}, response_status = {}) #:doc:
if options.is_a?(Hash) && options[:status]
status = options.delete(:status)
elsif response_status[:status]
status = response_status[:status]
else
status = request.post? ? 303 : 302
end
. . .
Which one do you think is better?
4. [1, 2, 3].to_xml
This works:
{ 'foo'=>'bar' }.to_xml
This throws with a violent exception:
[ 'foo', 'bar' ].to_xml
Any particular reason why it would be bad to XML-ize an array of primitive values?