Adapters

Read First: Resources Overview

View the Sample App

View the YARD Documentation

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 example, ElasticSearch.

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.

Association Macros

Read First: Nested Queries

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.