Module: APIHelper::Includable

Extended by:
ActiveSupport::Concern
Defined in:
lib/api_helper/includable.rb

Overview

Helper To Make Resource APIs Includable

Inclusion of related resource lets your API return resources related to the primary data. This endpoint will support an include request parameter to allow the client to customize which related resources should be returned.

This design made references to the rules of Inclusion of Related Resources in JSON API: jsonapi.org/format/#fetching-includes

For instance, comments could be requested with articles:

GET /articles?include=comments

The server will respond

[
  {
    "id": 1,
    "title": "First Post",
    "content": "...",
    "comments": [
      {
        "id": 1,
        "content": "..."
      },
      {
        "id": 3,
        "content": "..."
      },
      {
        "id": 6,
        "content": "..."
      }
    ]
  },
  {
    "id": 2,
    "title": "Second Post",
    "content": "...",
    "comments": [
      {
        "id": 2,
        "content": "..."
      },
      {
        "id": 4,
        "content": "..."
      },
      {
        "id": 5,
        "content": "..."
      }
    ]
  }
]

instead of just:

[
  {
    "id": 1,
    "title": "First Post",
    "content": "...",
    "comments": [1, 3, 6]
  },
  {
    "id": 2,
    "title": "Second Post",
    "content": "...",
    "comments": [2, 4, 5]
  }
]

If requesting multiple related resources is needed, they can be stated in a comma-separated list:

GET /articles/12?include=author,comments

Usage

Include this Concern in your Action Controller:

SamplesController < ApplicationController
  include APIHelper::Includable
end

or in your Grape API class:

class SampleAPI < Grape::API
  include APIHelper::Includable
end

then set the options for the inclusion in the grape method:

resources :posts do
  get do
    inclusion_for :post, root: true
    # ...
  end
end

This helper parses the include and include[object_type] parameters to determine what the API caller wants, and save the results into instance variables for further usage.

After this you can use the inclusion helper method to get the inclusion data that the request specifies, and do something like this in your controller:

resource = resource.includes(:author) if inclusion(:post, :author)

The inclusion helper method returns data like this:

inclusion #=> { post: [:author] }
inclusion(:post) #=> [:author]
inclusion(:post, :author) #=> true

API View with RABL

If you’re using RABL as the API view, it can be setup like this:

# set the includable and default inclusion fields of the view
set_inclusion :post, default_includes: [:author]

# set the details for all includable fields
set_inclusion_field :post, :author, :author_id

# extends the partial to show included fields
extends('extensions/includable_childs', locals: { self_resource: :post })

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.include_param_desc(example: nil, default: nil) ⇒ Object

Return the ‘include’ param description



195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/api_helper/includable.rb', line 195

def self.include_param_desc(example: nil, default: nil)
  if default.present?
    desc = "Returning compound documents that include specific associated objects, defaults to '#{default}'."
  else
    desc = "Returning compound documents that include specific associated objects."
  end
  if example.present?
    "#{desc} Example value: '#{example}'"
  else
    desc
  end
end

Instance Method Details

#inclusion(resource = nil, field = nil) ⇒ Object

Getter for the inclusion data.



183
184
185
186
187
188
189
190
191
192
# File 'lib/api_helper/includable.rb', line 183

def inclusion(resource = nil, field = nil)
  if resource.blank?
    @inclusion ||= {}
  elsif field.blank?
    (@inclusion ||= {})[resource] ||= []
  else
    return false if (try(:fieldset, resource).present? && !fieldset(resource, field))
    inclusion(resource).include?(field)
  end
end

#inclusion_for(resource, root: false, default_includes: []) ⇒ Object

Gets the include parameters, organize them into a @inclusion hash for model to use inner-join queries and/or templates to render relation attributes included. Following the URL rules of JSON API: jsonapi.org/format/#fetching-includes

Params:

resource

Symbol name of resource to receive the inclusion



144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/api_helper/includable.rb', line 144

def inclusion_for(resource, root: false, default_includes: [])
  @inclusion ||= Hashie::Mash.new
  @meta ||= Hashie::Mash.new

  # put the includes in place
  if params[:include].is_a? Hash
    @inclusion[resource] = params[:include][resource] || params[:include][resource]
  elsif root
    @inclusion[resource] = params[:include]
  end

  # splits the string into array of symbles
  @inclusion[resource] = @inclusion[resource] ? @inclusion[resource].split(',').map(&:to_sym) : default_includes
end

#set_inclusion(resource, default_includes: []) ⇒ Object

View Helper to set the inclusion and default_inclusion.



160
161
162
163
164
# File 'lib/api_helper/includable.rb', line 160

def set_inclusion(resource, default_includes: [])
  @inclusion ||= {}
  @inclusion_field ||= {}
  @inclusion[resource] = default_includes if @inclusion[resource].blank?
end

#set_inclusion_field(self_resource, field, id_field, class_name: nil, url: nil) ⇒ Object

View Helper to set the inclusion details.



167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/api_helper/includable.rb', line 167

def set_inclusion_field(self_resource, field, id_field, class_name: nil, url: nil)
  return if (@fieldset.present? && @fieldset[self_resource].present? && !@fieldset[self_resource].include?(field))

  @inclusion_field ||= {}
  @inclusion_field[self_resource] ||= []
  field_data = {
    field: field,
    id_field: id_field,
    class_name: class_name,
    url: url
  }
  @inclusion_field[self_resource] << field_data
  @fieldset[self_resource].delete(field) if @fieldset[self_resource].present?
end