Building an Adapter

If you find yourself repeatedly making customizations to a group of Resources and seek DRYer code, package those customizations into an Adapter. Here we’ll be starting from a previous how-to, How to Use Alternate ORMs.

You can follow along with the below code snippets, or view the diff on github.

Adapters are simpler than you might think. It’s little more than copy-pasting those low-level customizations into a common class.

Start by creating lib/elasticsearch_adapter.rb. Cut/past the sorting, pagination, and #resolve overrides from EmployeeResource into the adapter, turning into def methods along the way:

# lib/elasticsearch_adapter.rb
class ElasticsearchAdapter
  def paginate(scope, current_page, per_page)
    scope.metadata.pagination.current_page = current_page
    scope.metadata.pagination.per_page = per_page
    scope
  end

  def order(scope, att, dir)
    scope.metadata.sort = [{att: att, dir: dir}]
    scope
  end

  def resolve(scope)
    scope.query!
    scope.results
  end
end

Ensure our adapter gets loaded:

# config/initializers/jsonapi.rb
require 'elasticsearch_adapter'

And switch to that adapter in EmployeeResource:

use_adapter ElasticsearchAdapter

Bounce your server. You can still hit the /api/v1/employees endpoint with the same sort and paginate functionality, but the code has been moved to an adapter.

Let’s ensure our users can filter as well:

def filter(scope, att, val)
  scope.condition(att).eq(val)
end

For all the methods and functionality an adapter supports, see the Adapter documenation.

We probably also want has_many-style macros to avoid writing similar allow_sideload code time after time. Start by specifying where this functionality is defined, and add a has_many macro:

module Sideloading
  def has_many(association_name,
               scope:,
               resource:,
               foreign_key:,
               primary_key: :id,
               &blk)
    # our code will go here
    instance_eval(&blk) if blk
  end
end

def sideloading_module
  Sideloading
end

The instance_eval is there so we can always drop down to a lower-level customization in our Resource.

We can basically cut/paste our existing sideload code and rewrite it as variables:

scope do |parents|
  parent_ids = parents.map { |p| p.send(primary_key) }
  scope.call.condition(foreign_key).or(parent_ids.uniq.compact)
end

assign do |parents, children|
  parents.each do |p|
    relevant_children = children.select do |c|
      c.send(foreign_key) == p.send(primary_key)
    end
    p.send(:"#{association_name}=", relevant_children)
  end
end

You can now remove any customizations from your Resource classes. You can continue to build the adapter, adding belongs_to, statistics, and more. View the adapter documentation for the full API.