{{{
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”:https://github.com/apotonick/roar while using a nifty feature introduced “in version 0.10.2”:https://github.com/apotonick/roar/commit/46fb88d1a79338188a9d05d353fc721d6a058e4e.
h3. What’s In That Fruit Salad, Sir?
Since we keep having fruit salads “in the last posts”:http://nicksda.apotomo.de/2012/03/ruby-on-rest-5-learn-hypermedia-by-making-fruit-salad/ 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.
{"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).
h3. 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”:https://github.com/apotonick/ruby-on-rest/commit/ed55c56c73f97a2c8f58257659b4dc223bd17f3e.
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”:https://github.com/mislav/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.
h3. 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”:http://rubydoc.info/github/mislav/will_paginate/93e7b446900853d22e89/WillPaginate/Collection#total_entries-instance_method. 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”:http://nicksda.apotomo.de/2012/01/ruby-on-rest-3-one-model-multiple-representations/. 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.
h3. 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.
h3. 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)
h3. Cheers!
It is Friday eve, have a wonderful weekend and let me know how it was!
}}}
Congrats! Roar its always getting better!
LikeLike
These examples are very helpful to understand Roar and how to use it, thanks!
LikeLike
I haven’t been able to get the example code working… I’m working with Rails 3.2.1, and will_paginate 3.0.3.
I create a bowl, and paginate the fruits. Everything working there, page.to_json returns fine. Then I extend BowlPageRepresenter and .to_json gives me this error:
ActionController::RoutingError: No route matches {:action=>”show”, :controller=>”bowls”, :page=>page 1}
Might be a noob question, but, why is it routing anyway?
Other methods like to_xml work fine… probably because in this example they’re not overridden by ROAR like to_json. New methods like .page_url are also giving me similar errors.
Any clues? Thanks in advance!
LikeLike
In case anyone is wondering how to use a nested resource url when paginating, I came up with this: https://gist.github.com/jimmybaker/830c1619a8f84b30d514
I would love to hear suggestions for improvements.
LikeLike
HI!
I’m using this generic Pagination Representer, and it’s wokring like charm!
I’m using Kaminari gem instead of will_paginate.
But i’m having some trouble with params on the paging url.
Gist: https://gist.github.com/javifr/fd9037c0e26c607b9d32#file-issues-with-params-on-page_url
Do you think you could help me????
BEST!
JAVI
LikeLike