Pagination with ActiveResource

1367193600

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 or Kaminari, 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 greatest README, 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.