|
| 1 | +require 'active_support' |
| 2 | + |
| 3 | +# = Helper To Make Resource APIs Fieldsettable |
| 4 | +# |
| 5 | +# By making an API fieldsettable, you let API callers to choose the fields they |
| 6 | +# wanted to be returned with query parameters. This is really useful for making |
| 7 | +# API calls more efficient and fast. |
| 8 | +# |
| 9 | +# This design made references to the rules of <em>Sparse Fieldsets</em> in |
| 10 | +# <em>JSON API</em>: |
| 11 | +# http://jsonapi.org/format/#fetching-sparse-fieldsets |
| 12 | +# |
| 13 | +# A client can request that an API endpoint return only specific fields in the |
| 14 | +# response by including a +fields+ parameter, which is a comma-separated (",") |
| 15 | +# list that refers to the name(s) of the fields to be returned. |
| 16 | +# |
| 17 | +# GET /users?fields=id,name,avatar_url |
| 18 | +# |
| 19 | +# This functionality may also support requests specifying multiple fieldsets |
| 20 | +# for several objects at a time (e.g. another object included in an field of |
| 21 | +# another object) with <tt>fields[object_type]</tt> parameters. |
| 22 | +# |
| 23 | +# GET /posts?fields[posts]=id,title,author&fields[user]=id,name,avatar_url |
| 24 | +# |
| 25 | +# Note: +author+ of a +post+ is a +user+. |
| 26 | +# |
| 27 | +# The +fields+ and <tt>fields[object_type]</tt> parameters can not be mixed. |
| 28 | +# If the latter format is used, then it must be used for the main object as well. |
| 29 | +# |
| 30 | +# == Usage |
| 31 | +# |
| 32 | +# Include this +Concern+ in your Action Controller: |
| 33 | +# |
| 34 | +# SamplesController < ApplicationController |
| 35 | +# include APIHelper::Fieldsettable |
| 36 | +# end |
| 37 | +# |
| 38 | +# or in your Grape API class: |
| 39 | +# |
| 40 | +# class SampleAPI < Grape::API |
| 41 | +# include APIHelper::Fieldsettable |
| 42 | +# end |
| 43 | +# |
| 44 | +# then set the options for the fieldset in the grape method: |
| 45 | +# |
| 46 | +# resources :posts do |
| 47 | +# get do |
| 48 | +# fieldset_for :post, root: true, default_fields: [:id, :title, :author] |
| 49 | +# fieldset_for :user, permitted_fields: [:id, :name, :posts, :avatar_url], |
| 50 | +# show_all_permitted_fields_by_default: true |
| 51 | +# # ... |
| 52 | +# end |
| 53 | +# end |
| 54 | +# |
| 55 | +# This helper parses the +fields+ and <tt>fields[object_type]</tt> parameters to |
| 56 | +# determine what the API caller wants, and save the results into instance |
| 57 | +# variables for further usage. |
| 58 | +# |
| 59 | +# After this you can use the +fieldset+ helper method to get the fieldset data |
| 60 | +# that the request specifies. |
| 61 | +# |
| 62 | +# With <tt>GET /posts?fields=title,author</tt>: |
| 63 | +# |
| 64 | +# fieldset #=> { post: [:title, :author], user: [:id, :name, :posts, :avatar_url] } |
| 65 | +# |
| 66 | +# With <tt>GET /posts?fields[post]=title,author&fields[user]=name</tt>: |
| 67 | +# |
| 68 | +# fieldset #=> { post: [:title, :author], user: [:name] } |
| 69 | +# fieldset(:post) #=> [:title, :author] |
| 70 | +# fieldset(:post, :title) #=> true |
| 71 | +# fieldset(:user, :avatar_url) #=> false |
| 72 | +# |
| 73 | +# You can make use of the information while dealing with requests, for example: |
| 74 | +# |
| 75 | +# Post.select(fieldset(:post))... |
| 76 | +# |
| 77 | +# If you're using RABL as the API view, it can be also setup like this: |
| 78 | +# |
| 79 | +# object @user |
| 80 | +# |
| 81 | +# # this ensures the +fieldset+ instance variable is least setted with |
| 82 | +# # the default fields, and double check +permitted_fields+ at view layer - |
| 83 | +# # in case of things going wrong in the controller |
| 84 | +# set_fieldset :user, default_fields: [:id, :name, :avatar_url], |
| 85 | +# permitted_fields: [:id, :name, :avatar_url, :posts] |
| 86 | +# |
| 87 | +# # determine the fields to show on the fly |
| 88 | +# attributes(*fieldset[:user]) |
| 89 | +module APIHelper::Fieldsettable |
| 90 | + extend ActiveSupport::Concern |
| 91 | + |
| 92 | + # Gets the fields parameters, organize them into a +@fieldset+ hash for model to select certain |
| 93 | + # fields and/or templates to render specified fieldset. Following the URL rules of JSON API: |
| 94 | + # http://jsonapi.org/format/#fetching-sparse-fieldsets |
| 95 | + # |
| 96 | + # Params: |
| 97 | + # |
| 98 | + # +resource+:: |
| 99 | + # +Symbol+ name of resource to receive the fieldset |
| 100 | + # |
| 101 | + # +root+:: |
| 102 | + # +Boolean+ should this resource take the parameter from +fields+ while no type is specified |
| 103 | + # |
| 104 | + # +permitted_fields+:: |
| 105 | + # +Array+ of +Symbol+s list of accessible fields used to filter out unpermitted fields, |
| 106 | + # defaults to permit all |
| 107 | + # |
| 108 | + # +default_fields+:: |
| 109 | + # +Array+ of +Symbol+s list of fields to show by default |
| 110 | + # |
| 111 | + # +show_all_permitted_fields_by_default+:: |
| 112 | + # +Boolean+ if set to true, @fieldset will be set to all permitted_fields when the current |
| 113 | + # resource's fieldset isn't specified |
| 114 | + # |
| 115 | + # Example Result: |
| 116 | + # |
| 117 | + # fieldset_for :user, root: true |
| 118 | + # fieldset_for :group |
| 119 | + # |
| 120 | + # # @fieldset => { |
| 121 | + # # :user => [:id, :name, :email, :groups], |
| 122 | + # # :group => [:id, :name] |
| 123 | + # # } |
| 124 | + def fieldset_for(resource, root: false, permitted_fields: [], show_all_permitted_fields_by_default: false, default_fields: []) |
| 125 | + @fieldset ||= Hashie::Mash.new |
| 126 | + @meta ||= Hashie::Mash.new |
| 127 | + |
| 128 | + # put the fields in place |
| 129 | + if params[:fields].is_a? Hash |
| 130 | + @fieldset[resource] = params[:fields][resource] || params[:fields][resource] |
| 131 | + elsif root |
| 132 | + @fieldset[resource] = params[:fields] |
| 133 | + end |
| 134 | + |
| 135 | + # splits the string into array of symbles |
| 136 | + @fieldset[resource] = @fieldset[resource].present? ? @fieldset[resource].split(',').map(&:to_sym) : default_fields |
| 137 | + |
| 138 | + # filter out unpermitted fields by intersecting them |
| 139 | + @fieldset[resource] &= permitted_fields if @fieldset[resource].present? && permitted_fields.present? |
| 140 | + |
| 141 | + # set default fields to permitted_fields if needed |
| 142 | + @fieldset[resource] = permitted_fields if show_all_permitted_fields_by_default && @fieldset[resource].blank? && permitted_fields.present? |
| 143 | + end |
| 144 | + |
| 145 | + # View Helper to set the default and permitted fields |
| 146 | + def set_fieldset(resource, default_fields: [], permitted_fields: []) |
| 147 | + @fieldset ||= {} |
| 148 | + @fieldset[resource] = default_fields if @fieldset[resource].blank? |
| 149 | + @fieldset[resource] &= permitted_fields |
| 150 | + end |
| 151 | + |
| 152 | + # Getter for the fieldset data |
| 153 | + def fieldset(resource = nil, field = nil) |
| 154 | + if resource.blank? |
| 155 | + @fieldset ||= {} |
| 156 | + elsif field.blank? |
| 157 | + (@fieldset ||= {})[resource] ||= [] |
| 158 | + else |
| 159 | + fieldset(resource).include?(field) |
| 160 | + end |
| 161 | + end |
| 162 | + |
| 163 | + # Return the 'fields' param description |
| 164 | + def self.fields_param_desc(example: nil) |
| 165 | + if example.present? |
| 166 | + "Choose the fields to be returned. Example value: '#{example}'" |
| 167 | + else |
| 168 | + "Choose the fields to be returned." |
| 169 | + end |
| 170 | + end |
| 171 | +end |
0 commit comments