Sonntag, 9. November 2014

Respond with JSON using Rabl gem!

JSON format is the de facto standard especially compared to XML. Nowadays controllers responding with JSON is no novelty. Often times a controller responding with JSON looks as simple as:
class IngredientsController < ApplicationController
  respond_to :json

  def index
    respond_with Ingredients.all
  end
end
and requesting it:
curl http://localhost:3000/ingredients.json
=> [
  {"id":1,"created_at":"2014-08-09T19:08:44Z","name":"Basil","illegal":false},
  {"id":2,"created_at":"2014-08-09T19:08:56Z","name":"Ginger","illegal":false},
  {"id":3,"created_at":"2014-11-09T19:13:38Z","name":"Sassafras","illegal":true}
]
which JSONifies the found Ingredient objects and responds them in a very concise manner. It works fine for simple use cases.
But oftentimes use cases get more complex and then a primitive JSON mapping does not satisfy the requirements adequate. For example, when:
  1. the collection response is tremendous and responding only the relevant object attributes makes a huge payload difference
  2. the client receiver Javascript API expects a dedicated JSON object signature (e.g autocompleters often expect 'id' and 'name')
  3. the responding JSON objects require access to custom ActiveRecord methods
then hacking around ActiveRecord#to_json or even ActiveRecord#as_json has its limitation and is rarely convenient. Besides it is a view thing (just another representation of the objects) and requires templating.
Then the Rabl gem comes into play. It is a templating module for JSON objects, which provides the required flexibility.
It is easy included into the Ruby on Rails project (Gemfile):
gem 'rabl'
For example a new requirement wants an autocompleter to list the searched ingredients by their name with acronym in brackets. A solution could be refactoring the controller:
class IngredientsController < ApplicationController
  respond_to :json

  def index
    @ingredients = Ingredients.search params[:search]
    respond_with @ingredients
  end
end
and extending the model:
class Ingredient < ActiveRecord::Base
  def self.search term
    where("#{table_name}.name LIKE :term OR #{table_name}.description LIKE :term", { term: "%#{term}%" })
  end
end
Finally the view (ingredients/index.json.rabl):
collection @ingredients, object_root: false
attributes :id
node(:name) { |ingredient|
  text = ingredient.name 
  text << " (illegal)" if ingredient.illegal?
  text
}
needs some explanation. At first the instance variable @ingredients contains the ingredients collection. The default root node can be removed by setting the option object_root: false.
The required attributes are defined by assigning them to attributes and custom nodes can be defined by passing a block to node.
There are more options and the Rabl API is way more flexible when it comes to child nodes, gluing attributes, partials, inheritance, deep nesting, caching etc.
Requesting the ingredients:
curl http://localhost:3000/ingredients.json?search=as
=> [
  {"id":1,"name":"Basil"},
  {"id":3,"name":"Sassafras (illegal)"}
]
Responding to a request for a specific object could look like:
class IngredientsController < ApplicationController
  respond_to :json

  def show
    @ingredient = Ingredients.find params[:id]
    respond_with @ingredient
  end
end
and its view (ingredients/show.json.rabl):
object @ingredient
attributes id: :id, to_s: :title
child :foods, object_root: 'meal' do
  attribute :name
end
Requesting it:
curl http://localhost:3000/ingredients/1.json
=> {"ingredient":{"title":"Basil","foods":[
    {"meal":{"name":"Garlic Basil Shrimp"}},
    {"meal":{"name":"Basil and Lime Sorbet"}}
  ]
}}

Supported by Ruby 2.1.1 and Ruby on Rails 3.2.17