Module: APIHelper::Fieldsettable

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

Overview

Fieldsettable

By making an API fieldsettable, you enables the ability for API clients to choose the returned fields of resources with URL query parameters. This is really useful for optimizing requests, making API calls more efficient and fast.

This design made references to the rules of Sparse Fieldsets in JSON API: jsonapi.org/format/#fetching-sparse-fieldsets

A client can request to get only specific fields in the response by using the fields parameter, which is expected to be a comma-separated (“,”) list that refers to the name(s) of the fields to be returned.

GET /users?fields=id,name,avatar_url

This functionality may also support requests passing in multiple fieldsets for several resource at a time (e.g. an included related resource in an field of another resource) with fields[object_type] parameters.

GET /posts?fields[posts]=id,title,author&fields[user]=id,name,avatar_url

Note: author of a post is a user.

The fields and fields[object_type] parameters can not be mixed. If the latter format is used, then it must be used for the main resource as well.

Usage

Include this Concern in your Action Controller:

SamplesController < ApplicationController
  include APIHelper::Fieldsettable
end

or in your Grape API class:

class SampleAPI < Grape::API
  helpers APIHelper::Fieldsettable
end

Then set fieldset with fieldset_for for each resource in the controller:

def index
  fieldset_for :post, default: true, default_fields: [:id, :title, :author]
  fieldset_for :user, permitted_fields: [:id, :name, :posts, :avatar_url],
                      defaults_to_permitted_fields: true
  # ...
end

or in the Grape method if you're using Grape:

resources :posts do
  get do
    fieldset_for :post, default: true, default_fields: [:id, :title, :author]
    fieldset_for :user, permitted_fields: [:id, :name, :posts, :avatar_url],
                        defaults_to_permitted_fields: true
    # ...
  end
end

The fieldset_for method used above parses the fields and/or fields[resource_name] parameters, and save the results into @fieldset instance variable for further usage.

After that line, you can use the fieldset helper method to get the fieldset information. Actual examples are:

With GET /posts?fields=title,author:

fieldset #=> { post: [:title, :author], user: [:id, :name, :posts, :avatar_url] }

With GET /posts?fields[post]=title,author&fields[user]=name:

fieldset #=> { post: [:title, :author], user: [:name] }
fieldset(:post) #=> [:title, :author]
fieldset(:post, :title) #=> true
fieldset(:user, :avatar_url) #=> false

You can make use of these information while dealing with requests in the controller, for example:

Post.select(fieldset(:post)).find(params[:id])

And return only specified fields in the view, for instance, Jbuilder:

json.(@post, *fieldset(:post))
json.author do
  json.(@author, *fieldset(:user))
end

or RABL:

# post.rabl

object @post
attributes(*fieldset[:post])
child :author do
  extends 'user'
end

# user.rabl

object @user
attributes(*fieldset[:user])

You can also set properties of fieldset with the set_fieldset helper method in the views if you're using a same view across multiple controllers, for decreasing code duplication or increasing security. Below is an example with RABL:

object @user

# this ensures that the +fieldset+ instance variable is least setted with
# the default fields, and double filters +permitted_fields+ at view layer -
# in case of any things going wrong in the controller
set_fieldset :user, default_fields: [:id, :name, :avatar_url],
                    permitted_fields: [:id, :name, :avatar_url, :posts]

# determine the fields to show on the fly
attributes(*fieldset[:user])

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.fields_param_desc(example: nil) ⇒ Object

Returns the description of the 'fields' URL parameter


246
247
248
249
250
251
252
# File 'lib/api_helper/fieldsettable.rb', line 246

def self.fields_param_desc(example: nil)
  if example.present?
    "Choose the fields to be returned. Example value: '#{example}'"
  else
    "Choose the fields to be returned."
  end
end

Instance Method Details

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

Getter for the fieldset data

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

fieldset  # => { 'user' => ['name'], 'post' => ['title', 'author'] }

If one parameter - a specific resourse name is passed in, it will return a fields array of that specific resourse.

fieldset(:post)  # => ['title', 'author']

And if one more parameter - a field name, is passed in, it will return a boolen, determining if that field should exist in that resource.

fieldset(:post, :title)  # => true

217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/api_helper/fieldsettable.rb', line 217

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

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

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

#fieldset_for(resource, default: false, permitted_fields: [], defaults_to_permitted_fields: false, default_fields: []) ⇒ Object

Gets the fields parameters, organize them into a @fieldset hash for model to select certain. fields and/or templates to render specified fieldset. Following the URL rules of JSON API: jsonapi.org/format/#fetching-sparse-fieldsets

Params:

resource

Symbol name of resource to receive the fieldset

default

Boolean should this resource take the parameter from fields while no

resourse name is specified?
permitted_fields

Array of Symbols list of accessible fields used to filter out unpermitted fields, defaults to permit all

default_fields

Array of Symbols list of fields to show by default

defaults_to_permitted_fields

Boolean if set to true, @fieldset will be set to all permitted_fields when the current resource's fieldset isn't specified

Example Result:

fieldset_for :user, root: true
fieldset_for :group

# @fieldset => {
#                :user => [:id, :name, :email, :groups],
#                :group => [:id, :name]
#              }

165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/api_helper/fieldsettable.rb', line 165

def fieldset_for(resource, default: false,
                           permitted_fields: [],
                           defaults_to_permitted_fields: false,
                           default_fields: [])
  @fieldset ||= ActiveSupport::HashWithIndifferentAccess.new

  # put the fields in place
  if params[:fields].is_a?(Hash)
    # get the specific resource fields from fields hash
    @fieldset[resource] = params[:fields][resource] || params[:fields][resource]
  elsif default
    # or get the fields string directly if this resource is th default one
    @fieldset[resource] = params[:fields]
  end

  # splits the string into array
  if @fieldset[resource].present?
    @fieldset[resource] = @fieldset[resource].split(',').map(&:to_s)
  else
    @fieldset[resource] = default_fields.map(&:to_s)
  end

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

    # filter out unpermitted fields by intersecting them
    @fieldset[resource] &= permitted_fields if @fieldset[resource].present?

    # set default fields to permitted_fields if needed
    @fieldset[resource] = permitted_fields if @fieldset[resource].blank? &&
                                              defaults_to_permitted_fields
  end
end

#set_fieldset(resource, default_fields: [], permitted_fields: []) ⇒ Object

View Helper to set the default and permitted fields

This is useful while using an resource view shared by multiple controllers, it will ensure the @fieldset instance variable presents, and can also set the default fields of a model for convenience, or the whitelisted permitted fields for security.


239
240
241
242
243
# File 'lib/api_helper/fieldsettable.rb', line 239

def set_fieldset(resource, default_fields: [], permitted_fields: [])
  @fieldset ||= ActiveSupport::HashWithIndifferentAccess.new
  @fieldset[resource] = default_fields.map(&:to_s) if @fieldset[resource].blank?
  @fieldset[resource] &= permitted_fields.map(&:to_s) if permitted_fields.present?
end