Posts Tagged pagination

Ruby On REST 6: Pagination With Roar

Friday, May 18th, 2012

Yo! How’s it folks? Let’s do some more REST today. I’d like to show how easy it is to have paginated REST documents with Roar while using a nifty feature introduced in version 0.10.2.

What’s In That Fruit Salad, Sir?

Since we keep having fruit salads in the last posts I wanna write a service to display the ingredients of a particular fruit bowl in a list. Not just a list of fruits, a paginated list of fruits.

Let’s GET it.

POST http://bowls/1/fruits?page=1

Considering two fruit items per page this document will be returned.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{"total_entries":5,
 "links":[
  {"rel": "self",
   "href":"http://bowls/1/fruits?page=1"},
  {"rel": "next",
   "href":"http://bowls/1/fruits?page=2"}],
 "items":[
  {"title":"Apple",
   "links":[{"rel":"self",
              "href":"http://fruits/Apple"}]},
  {"title":"Orange",
   "links":[{"rel":"self",
             "href":"http://fruits/Orange"}]}
  ]
}

Ignore the items for now, the interesting part here are the pagination elements in the first lines. What we have here is:

  • The number of total ingredients in the bowl (line 1).
  • The obligatory self link (line 3-4).
  • A link guiding us to the next page (line 5-6).

Wow! Code?

The backing code in this service endpoint could look like the following snippet. As always, the code used to create this example can be found on github.

1
2
3
4
5
6
bowl = Bowl.find(1)
page = bowl.fruits.paginate(:page => 1, 
  :per_page => 2)
 
page.extend(BowlPageRepresenter)
page.to_json(:bowl => @bowl)

First task is to retrieve the viewed bowl (line 1). I use a static id in this example, feel free to replace it with something like params[:id] in your code. Next, I use paginate from the famous will_paginate gem to get a paged subset of the included fruits. Again, the :page parameter should be refering to a variable, whatever (line 2-3).

Then I simply extend the collection (line 5) since it is a valid Ruby object and call to_json to render the list (line 6). Keep in mind that we pass the :bowl instance into the render method as an argument.

The Bowl Representer Can Do Pagination

To fully understand that example we need to look at the representer now.

module BowlPageRepresenter
  include Roar::Representer::JSON
  include Roar::Representer::Feature::Hypermedia
 
  property :total_entries
  collection :items, :extend => FruitRepresenter
 
  link :self do |opts|
    bowl_url(opts[:bowl], :page => current_page)
  end
 
  link :next do |opts|
    bowl_url(opts[:bowl], :page => next_page) \
      if next_page
  end
 
  link :previous do |opts|
    bowl_url(opts[:bowl], :page => previous_page) \
      if previous_page
  end
 
  def items
    self
  end
end

Instead of refering to line number I’ll use code excerpts now. Do you like that better?

  property :total_entries

In a paginated collection we got the total_entries method as described in the will_paginate API docs. To give our REST consumer a hint about the total amount of ingredients we can simply define a property after that.

  collection :items, :extend => FruitRepresenter

To render the actual items in the doc we extend each fruit with the FruitRepresenter as we learned in an older post. Note that Roar will use the items method to retrieve the collection of fruits.

  def items
    self
  end

Since we are already a collection all we have to do is return self – roar will iterate the paginated collection, extend each element with the FruitRepresenter and render the items.

A Cool New Feature!

The links to the next pages are defined using Roar’s hypermedia feature.

  link :next do |opts|
    bowl_url(opts[:bowl], :page => next_page) \
      if next_page
  end

Two things happen here. First, note that this link is conditional. If next_page, another API method for a will_paginate collection, is evaluated to false, this link won’t be rendered.

Second, we use a new feature of Roar here to access variables passed into to_json. Remember how we called the render method?

page.to_json(:bowl => @bowl)

Right, we pass in some values from the outside since we don’t have access to the actual bowl instance within the represented collection. These parameters are accessible in the link block parameters. Isn’t that nice? I like it.

These few lines of code make it easy to render a paginated collection into a valid REST document.

Writing A Generic Pagination Representer

Now that all paginated documents share attributes (total entries, next and previous link, the self link, etc) why not abstract that into a generic representer? Most of the code can be reused.

module PaginationRepresenter
  include Roar::Representer::JSON
  include Roar::Representer::Feature::Hypermedia
 
  property :total_entries
 
  link :self do |opts|
    page_url(opts[:model], :page => current_page)
  end
 
  link :next do |opts|
    page_url(opts[:model], :page => next_page) \
      if next_page
  end
 
  link :previous do |opts|
    page_url(opts[:model], :page => previous_page) \
      if previous_page
  end
 
  def items
    self
  end
 
  def page_url(*)
    raise "Implement me."
  end
end

All I did was calling a generic page_url method that must be implemented by the using representer. Also, I no longer use the :bowl keyword but a more generic :model, ok?

Nothing in this abstract representer module is related to stinky fruit salads anymore. Man, this thing could even represent a sixpack of beers (a domain I do prefer over fruits).

To inherit we just include the abstract into the concrete representer.

module BowlPageRepresenter
  include Roar::Representer::JSON
  include PaginationRepresenter
 
  collection :items, :extend => FruitRepresenter
 
  def page_url(*args)
    bowl_url(*args)
  end
end

That is cool. All we have to do is defining the concrete items collection and how to compute the pagination URLs. Come on guys, that is easy!

Remember, we changed the incoming parameter, so the rendering call must change, too.

page.to_json(:model => @bowl)

Cheers!

It is Friday eve, have a wonderful weekend and let me know how it was!