Sonntag, 30. März 2014

Enhancing the Rails controller API RESTfully

If the 7 standard RESTful actions (read A simple Ruby on Rails route) are not enough, this one is for you.
The following example will show how, to add an additional RESTful route.
The original controller code example only lists all people:
class TasksController < ApplicationController
  def index
    @tasks = Task.all
  end
end
and the routes.rb:
resources :tasks, :only => :index
and finally the index.html.erb view:
<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Duration</th>
    </tr>
  </thead>
  <tbody>
    <% @tasks.each do |task| %>
      <tr>
        <td><%= task.name %></td>
        <td><%= task.duration %></td>
      </tr>
    <% end %>
  </tbody>
</table>
The new requirement is to add a personalized filter to the tasks collection. That's why I add a second action to the controller:
class TasksController < ApplicationController
  def index
    @tasks = filter_tasks
  end

  def search
    session[:duration] = params[:duration]
    @tasks = filter_tasks
    render :action => :index
  end
private
  def filter_tasks
    Task.duration(session[:duration]).all
  end
end
The new search action stores a filter parameter (duration) into the personalized session and finds all tasks depending on their duration. The finder was extracted into the private method (filter_tasks), because the same query is also needed in the index action for consistency reasons.
A model class method (duration) was used as a scoped finder.
Therefore the model:
class Task < ActiveRecords::Base
  def self.duration duration=nil
    return scoped if duration.nil?
    where :duration => duration
  end
end
The search action ends with rendering the same template as the index action.
The next step is to add the custom route into the routes.rb:
resources :tasks, :only => :index do 
  post 'search', :on => :collection
end
Please note, the new route is a collection route. It returns a collection of tasks. And it uses the HTTP verb 'POST' for posting the filter parameters to the server.
Doing rake routes in the console:
tasks GET /people/:person_id/tasks(.:format) tasks#index
search_tasks POST /tasks/search(.:format) tasks#search
In the index.html.erb the search URL helper method is used to generate the filter form:
<%= form_tag search_patients_path do %>
  <%= text_field_tag :duration %>
<% end %>
<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Duration</th>
    </tr>
  </thead>
  <tbody>
    <% @tasks.each do |task| %>
      <tr>
        <td><%= task.name %></td>
        <td><%= task.duration %></td>
      </tr>
    <% end %>
  </tbody>
</table>
I admit, sometimes the world is not as simple as examples and tutorials often pretend. That's why there are situations that seem to force you to extend the controller API with a new REStful action. But those cases are rare. Often times there is an option to stick to the 7 standard actions. So always think twice before you want to extend a controller API and always keep the REST approach in mind when doing it, because:
Every additional public method means accepting the responsibility for it.

Supported by Ruby 2.1.1 and Ruby on Rails 3.2.3