Recent versions of ActiveResource allow you to parse HTTP responses (without hacks).
For those unfamiliar, what is ActiveResource?
ActiveResource is a gem extracted from Rails. It is built on a standard JSON or XML format for requesting and submitting resources over HTTP.
The basic documentation is very well explained and can help you get started fairly fast. Unfortunately, once you begin to grow your application (and depend on ARes), you’ll notice that some simple tasks can quickly become rather cumbersome to figure out. Pagination is one of the first bumps I hit.
The solution
The first way to implement pagination, and my preferred choice, is to send pagination data along HTTP headers. This approach sends a clean response and follows API best practices. The downside to it is that it can be complex to implement.
Example with a Rails API and the will_paginate
gem
# app/controllers/api/posts_controller.rb
class Api::PostsController < ApiController
set_pagination_headers only: [:index]
def index
@posts = Post.paginate(:page => params[:page], :per_page => params[:per_page])
respond_with(@posts)
end
end
Nothing new here, the paginate
method is provided by the will_paginate
gem. However, note the set_pagination_headers
method, which is defined in the ApiController
class ApiController < ApplicationController
respond_to :json, :xml
private
def self.set_pagination_headers(options = {})
after_filter(options) do |controller|
results = instance_variable_get("@#{controller_name}") # @posts
headers["Pagination-Limit"] = results.limit_value.to_s
headers["Pagination-Offset"] = results.offset_value.to_s
headers["Pagination-TotalCount"] = results.total_count.to_s
end
end
end
See, limit_value
, offset
and total_count
are provided by will_paginate
, and I chose to send these values because I use the Kaminari
gem with ActiveResource in my client. Kaminari has an extension for paginatable arrays, which requires precisely these variables to figure out pagination logic. The set up is done once in an after_filter
, and we embed this data into our response headers.
Note: The best practice for custom HTTP headers was to prefix them with “X-“, but that approach has been deprecated recently.
That’s all for our API. Now we turn to the client.
ActiveResource 4.0.0.beta1
introduces ActiveResource::Collection
, which (according to the documentation in the source code) is a wrapper to handle parsing index responses. A Post
class can be set up to handle it with:
require 'active_resource/paginated_collection' # our custom parser
class Post < ActiveResource::Base
self.site = "http://example.com"
self.collection_parser = PaginatedCollection
end
The PaginatedCollection
class will take the parsed elements. Here we can access the connection object, which holds the response headers we set in our API.
Remember: This class inherits from ActiveResource::Collection
and there are certain methods we can override to customise our collection object.
# lib/active_resource/paginated_collection.rb
class PaginatedCollection < ActiveResource::Collection
# Our custom array to handle pagination methods
attr_accessor :paginatable_array
# The initialize method will receive the ActiveResource parsed result
# and set @elements.
def initialize(elements = [])
@elements = elements
setup_paginatable_array
end
# Retrieve response headers and instantiate a paginatable array
def setup_paginatable_array
@paginatable_array ||= begin
response = ActiveResource::Base.connection.response rescue {}
options = {
limit: response["Pagination-Limit"].try(:to_i),
offset: response["Pagination-Offset"].try(:to_i),
total_count: response["Pagination-TotalCount"].try(:to_i)
}
Kaminari::PaginatableArray.new(elements, options)
end
end
private
# Delegate missing methods to our `paginatable_array` first,
# Kaminari might know how to respond to them
# E.g. current_page, total_count, etc.
def method_missing(method, *args, &block)
if paginatable_array.respond_to?(method)
paginatable_array.send(method)
else
super
end
end
end
Note: You must install the Kaminari gem for this to work.
Now your ActiveResource collections can handle pagination:
# client - app/controllers/posts_controller.rb
class PostsController < ApplicationController
def index
@posts = Post.all(params: { page: params[:page], per_page: params[:per_page]})
respond_with(@posts)
end
end
And your views too (thanks to Kaminari):
<!-- client - app/views/posts/index.html.erb -->
<div id="posts">
<h2>Posts</h2>
<ul>
<%= render @posts %>
</ul>
<%= paginate @posts %>
</div>
Other options
Another alternative is to send pagination data inside the body of the response
# GET /posts.json:
# {
# posts: [
# {
# title: "Foo",
# body: "Lorem Ipsum"
# }
# { ... }
# ]
# next_page: "/posts.json?page=2"
# }
@elements = parsed['posts']
@next_page = parsed['next_page']
You will have to make some modifications to the code above in order for this to work.
Final words
I encourage you to try the first approach. It embraces the principles of flexibility, standardization and loose coupling.
Final notes
- You can use either
will_paginate
orKaminari
, or both (like in this example), or even another solution. As long as you can create custom paginatable arrays. I chose Kaminari for the client because it was easier and I read it handles arrays pretty well. - Solution built with ideas from this blog post and the active_resource source code
ActiveResource
might not have the greatestREADME
, but you can find everything you need in the comments inside the source code, and in the tests, so make sure to take a look there.