Module: APIHelper::Includable

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

Overview

Includable

Inclusion lets your API returns not only the data of the primary resource, but also resources that have relation to it. Includable APIs will also support customising the resources included using the include parameter.

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

For instance, articles can be requested with their comments along:

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 the ids of each comment

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

Multiple related resources can be stated in a comma-separated list, like this:

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
  helpers APIHelper::Includable
end

Then setup inclusion with inclusion_for in the controller:

def index
  inclusion_for :post, default: true
  # ...
end

or in the Grape method if you’re using it:

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

This helper parses the include and/or include[resource_name] parameters and saves the results into @inclusion for further usage.

Includable integrates with Fieldsettable if used together, by:

  • Sliceing the included fields that dosen’t appears in the fieldset - since the included resoure(s) are actually fields under the primary resorce, fieldset will be in charged to determine the fields to show. Thus, fields will be totally ignored if they aren’t appeared in the fieldset, regardless if they are included or not.

So notice that inclusion_for should be set after fieldset_for if both are used!

After that inclusion_for … line, you can use the inclusion helper method to get the inclusion data of each request, and do something like this in your controller:

@posts = Post.includes(inclusion(:post))

The inclusion helper method will return data depending on the parameters passed in, as the following example:

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

And don’t forget to set your API views or serializers with the help of inclusion to provide dynamic included resources!

API View with RABL

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

object @post

# 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
set_inclusion_field :post, :comments, :comment_ids

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

– TODO: provide an example of includable_childs.rabl ++

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

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

Returns the description of the ‘include’ URL parameter



337
338
339
340
341
342
343
344
345
346
347
348
349
# File 'lib/api_helper/includable.rb', line 337

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

This method will act as a traditional getter of the inclusion data and returns a hash containing fields for each resource if no parameter is provided.

inclusion  # => { 'post' => ['author', 'comments'] }

If one parameter - a specific resourse name is passed in, it will return an array of relation names that should be included for that specific resourse.

inclusion(:post)  # => ['author', 'comments']

And if one more parameter - a field name, is passed in, it will return a boolen, determining if that relation should be included in the response.

inclusion(:post, :author)  # => true


248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
# File 'lib/api_helper/includable.rb', line 248

def inclusion(resource = nil, field = nil)
  # act as a traditional getter if no parameters specified
  if resource.blank?
    @inclusion ||= ActiveSupport::HashWithIndifferentAccess.new

  # returns the inclusion array if an specific resource is passed in
  elsif field.blank?
    inclusion[resource] || []

  # determine if a field is inculded
  else
    field = field.to_s
    inclusion(resource).is_a?(Array) && inclusion(resource).include?(field)
  end
end

#inclusion_field(resource = nil) ⇒ Object

Getter for the data of includable fields

Params:

resource

Symbol the resource name of inclusion data to retrieve



325
326
327
328
329
330
331
332
333
334
# File 'lib/api_helper/includable.rb', line 325

def inclusion_field(resource = nil)
  # act as a traditional getter if no parameters specified
  if resource.blank?
    @inclusion_field ||= ActiveSupport::HashWithIndifferentAccess.new

  # returns the inclusion array if an specific resource is passed in
  else
    inclusion_field[resource] || {}
  end
end

#inclusion_for(resource, default: false, permitted_includes: [], defaults_to_permitted_includes: false, default_includes: []) ⇒ Object

Gets the include parameters, organize them into a @inclusion hash

Params:

resource

Symbol name of resource to receive the inclusion

default

Boolean should this resource take the parameter from include while no

resourse name is specified?
permitted_includes

Array of Symbols list of includable fields, permitting all by default

default_includes

Array of Symbols list of fields to be included by default

defaults_to_permitted_includes

Boolean if set to true, @inclusion will be set to all permitted_includes when the current resource’s included fields isn’t specified



187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/api_helper/includable.rb', line 187

def inclusion_for(resource, default: false,
                            permitted_includes: [],
                            defaults_to_permitted_includes: false,
                            default_includes: [])
  @inclusion ||= ActiveSupport::HashWithIndifferentAccess.new
  @inclusion_specified ||= ActiveSupport::HashWithIndifferentAccess.new

  # put the fields in place
  if params[:include].is_a?(Hash) ||
     defined?(Rails) && Rails.version.to_i >= 5 && params[:include].is_a?(ActionController::Parameters)
    # get the specific resource inclusion fields from the "include" hash
    @inclusion[resource] = params[:include][resource]
    @inclusion_specified[resource] = true if params[:include][resource].present?
  elsif default
    # or get the "include" string directly if this resource is th default one
    @inclusion[resource] = params[:include]
    @inclusion_specified[resource] = true if params[:include].present?
  end

  # splits the string into array
  if @inclusion[resource].present?
    @inclusion[resource] = @inclusion[resource].split(',').map(&:to_s)
  elsif !@inclusion_specified[resource]
    @inclusion[resource] = default_includes.map(&:to_s)
  end

  if permitted_includes.present?
    permitted_includes = permitted_includes.map(&:to_s)

    # filter out unpermitted includes by intersecting them
    @inclusion[resource] &= permitted_includes if @inclusion[resource].present?

    # set default inclusion to permitted_includes if needed
    @inclusion[resource] = permitted_includes if @inclusion[resource].blank? &&
                                                 defaults_to_permitted_includes &&
                                                 !@inclusion_specified[resource]
  end

  if @fieldset.is_a?(Hash) && @fieldset[resource].present?
    @inclusion[resource] &= @fieldset[resource]
  end
end

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

View Helper to set the inclusion

This is useful while using an resource view shared by multiple controllers, this will ensure the @inclusion instance variable presents, and can also set the default included fields of a model for convenience, or the fields that are permitted to be included for security.



270
271
272
273
274
275
276
277
278
279
280
281
# File 'lib/api_helper/includable.rb', line 270

def set_inclusion(resource, default_includes: [], permitted_includes: [])
  @inclusion ||= ActiveSupport::HashWithIndifferentAccess.new
  @inclusion_field ||= ActiveSupport::HashWithIndifferentAccess.new
  @inclusion_specified ||= ActiveSupport::HashWithIndifferentAccess.new
  @inclusion[resource] = default_includes.map(&:to_s) if @inclusion[resource].blank? &&
                                                         !@inclusion_specified[resource]
  @inclusion[resource] &= permitted_includes.map(&:to_s) if permitted_includes.present?

  if @fieldset.is_a?(Hash) && @fieldset[resource].present?
    @inclusion[resource] &= @fieldset[resource]
  end
end

#set_inclusion_field(resource, field, id_field, resource_name: nil, resources_url: nil) ⇒ Object

View Helper to set the inclusion details

Params:

resource

Symbol name of the resource to receive the inclusion field data

field

Symbol the field name of the relatiion that can be included

id_field

Symbol the field to use (normally suffixed with “_id”) if the object isn’t included

resource_name

Symbol the name of the child resource, can be used to determine which view template should be extended for rendering that child node and also can shown in the response metadata as well

resources_url

String the resources URL of the child resource, can be used to be shown in the metadata for the clients’ convenience to learn ablou the API



306
307
308
309
310
311
312
313
314
315
316
# File 'lib/api_helper/includable.rb', line 306

def set_inclusion_field(resource, field, id_field, resource_name: nil,
                                                   resources_url: nil)
  @inclusion_field ||= ActiveSupport::HashWithIndifferentAccess.new
  @inclusion_field[resource] ||= ActiveSupport::HashWithIndifferentAccess.new
  @inclusion_field[resource][field] = {
    field: field,
    id_field: id_field,
    resource_name: resource_name,
    resources_url: resources_url
  }
end