api_helper 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +6 -0
- data/Appraisals +23 -0
- data/README.md +7 -7
- data/api_helper.gemspec +4 -0
- data/examples/includable_childs.rabl +0 -0
- data/gemfiles/grape_0.10.0.gemfile +8 -0
- data/gemfiles/grape_0.11.0.gemfile +8 -0
- data/gemfiles/rails_4.0.0.gemfile +8 -0
- data/gemfiles/rails_4.1.0.gemfile +8 -0
- data/gemfiles/rails_4.1.8.gemfile +8 -0
- data/gemfiles/rails_4.2.0.gemfile +8 -0
- data/lib/api_helper/fieldsettable.rb +141 -53
- data/lib/api_helper/filterable.rb +66 -26
- data/lib/api_helper/includable.rb +185 -60
- data/lib/api_helper/version.rb +1 -1
- metadata +67 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 326e1e9e2c1b9509990e1103a0d8f95da5be83a9
|
4
|
+
data.tar.gz: 4c9fd145828a49a586d8a4b7dee67fa177ea957c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d14e69fb29488afe354b941fc4501ba92620936731673a2bc41274cf37f77bd4f2fbcf7e368e9dd8cc52e473a4b9b8ce1fd1b49391d59f6e638df0fac6452eda
|
7
|
+
data.tar.gz: f44a72e00cc934ad01194d0c20fa799f0fc6e232d26abced40f6a60b120b6942b66db0d3e21bfa90496ecc10e0b51b84833ad77727ebb2b85363dd1a5c7e5177
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/Appraisals
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
appraise 'rails-4.1.0' do
|
2
|
+
gemspec
|
3
|
+
gem 'rails', '4.1.0'
|
4
|
+
gem 'rspec-rails'
|
5
|
+
end
|
6
|
+
|
7
|
+
appraise 'rails-4.2.0' do
|
8
|
+
gemspec
|
9
|
+
gem 'rails', '4.2.0'
|
10
|
+
gem 'rspec-rails'
|
11
|
+
end
|
12
|
+
|
13
|
+
appraise 'grape-0.10.0' do
|
14
|
+
gemspec
|
15
|
+
gem 'grape', '0.10.0'
|
16
|
+
gem 'rack-test'
|
17
|
+
end
|
18
|
+
|
19
|
+
appraise 'grape-0.11.0' do
|
20
|
+
gemspec
|
21
|
+
gem 'grape', '0.11.0'
|
22
|
+
gem 'rack-test'
|
23
|
+
end
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# APIHelper
|
1
|
+
# APIHelper [![Gem Version](https://badge.fury.io/rb/api_helper.svg)](http://badge.fury.io/rb/api_helper) [![Build Status](https://travis-ci.org/Neson/api_helper.svg?branch=master)](https://travis-ci.org/Neson/api_helper) [![Docs Status](https://inch-ci.org/github/Neson/api_helper.svg?branch=master)](https://inch-ci.org/github/Neson/api_helper)
|
2
2
|
|
3
3
|
Helpers for creating standard RESTful API for Rails or Grape with Active Record.
|
4
4
|
|
@@ -8,7 +8,7 @@ Helpers for creating standard RESTful API for Rails or Grape with Active Record.
|
|
8
8
|
Add this line to your application's Gemfile:
|
9
9
|
|
10
10
|
```ruby
|
11
|
-
gem '
|
11
|
+
gem 'api_helper'
|
12
12
|
```
|
13
13
|
|
14
14
|
And then execute:
|
@@ -17,7 +17,7 @@ And then execute:
|
|
17
17
|
|
18
18
|
Or install it yourself as:
|
19
19
|
|
20
|
-
$ gem install
|
20
|
+
$ gem install api_helper
|
21
21
|
|
22
22
|
|
23
23
|
## API Standards
|
@@ -62,7 +62,7 @@ PostsController < ApplicationController
|
|
62
62
|
end
|
63
63
|
```
|
64
64
|
|
65
|
-
Further usage of each helper can be found in the docs.
|
65
|
+
Further usage of each helper can be found in the [docs](http://www.rubydoc.info/github/Neson/api_helper/master/APIHelper).
|
66
66
|
|
67
67
|
### Grape
|
68
68
|
|
@@ -79,16 +79,16 @@ class PostsAPI < Grape::API
|
|
79
79
|
end
|
80
80
|
```
|
81
81
|
|
82
|
-
Further usage of each helper can be found in the docs.
|
82
|
+
Further usage of each helper can be found in the [docs](http://www.rubydoc.info/github/Neson/api_helper/master/APIHelper).
|
83
83
|
|
84
84
|
|
85
85
|
## Development
|
86
86
|
|
87
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake
|
87
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `appraisal rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
88
88
|
|
89
89
|
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
90
90
|
|
91
91
|
|
92
92
|
## Contributing
|
93
93
|
|
94
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/Neson/
|
94
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/Neson/api_helper.
|
data/api_helper.gemspec
CHANGED
@@ -20,7 +20,11 @@ Gem::Specification.new do |spec|
|
|
20
20
|
|
21
21
|
spec.add_development_dependency "bundler"
|
22
22
|
spec.add_development_dependency "rake"
|
23
|
+
spec.add_development_dependency "appraisal"
|
23
24
|
spec.add_development_dependency "rspec"
|
25
|
+
spec.add_development_dependency "byebug"
|
26
|
+
spec.add_development_dependency "activerecord"
|
27
|
+
spec.add_development_dependency "sqlite3"
|
24
28
|
|
25
29
|
spec.add_dependency "activesupport", ">= 3"
|
26
30
|
end
|
File without changes
|
@@ -1,31 +1,34 @@
|
|
1
1
|
require 'active_support'
|
2
|
+
require 'active_support/core_ext/object/blank'
|
2
3
|
|
3
|
-
# =
|
4
|
+
# = Fieldsettable
|
4
5
|
#
|
5
|
-
# By making an API fieldsettable, you
|
6
|
-
#
|
7
|
-
# API calls more efficient and
|
6
|
+
# By making an API fieldsettable, you enables the ability for API clients to
|
7
|
+
# choose the returned fields of resources with URL query parameters. This is
|
8
|
+
# really useful for optimizing requests, making API calls more efficient and
|
9
|
+
# fast.
|
8
10
|
#
|
9
11
|
# This design made references to the rules of <em>Sparse Fieldsets</em> in
|
10
12
|
# <em>JSON API</em>:
|
11
13
|
# http://jsonapi.org/format/#fetching-sparse-fieldsets
|
12
14
|
#
|
13
|
-
# A client can request
|
14
|
-
#
|
15
|
-
#
|
15
|
+
# A client can request to get only specific fields in the response by using
|
16
|
+
# the +fields+ parameter, which is expected to be a comma-separated (",") list
|
17
|
+
# that refers to the name(s) of the fields to be returned.
|
16
18
|
#
|
17
19
|
# GET /users?fields=id,name,avatar_url
|
18
20
|
#
|
19
|
-
# This functionality may also support requests
|
20
|
-
# for several
|
21
|
-
# another
|
21
|
+
# This functionality may also support requests passing in multiple fieldsets
|
22
|
+
# for several resource at a time (e.g. an included related resource in an field
|
23
|
+
# of another resource) with <tt>fields[object_type]</tt> parameters.
|
22
24
|
#
|
23
25
|
# GET /posts?fields[posts]=id,title,author&fields[user]=id,name,avatar_url
|
24
26
|
#
|
25
27
|
# Note: +author+ of a +post+ is a +user+.
|
26
28
|
#
|
27
29
|
# 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
|
30
|
+
# If the latter format is used, then it must be used for the main resource as
|
31
|
+
# well.
|
29
32
|
#
|
30
33
|
# == Usage
|
31
34
|
#
|
@@ -38,26 +41,35 @@ require 'active_support'
|
|
38
41
|
# or in your Grape API class:
|
39
42
|
#
|
40
43
|
# class SampleAPI < Grape::API
|
41
|
-
#
|
44
|
+
# helpers APIHelper::Fieldsettable
|
42
45
|
# end
|
43
46
|
#
|
44
|
-
#
|
47
|
+
# Then set fieldset with +fieldset_for+ for each resource in the controller:
|
48
|
+
#
|
49
|
+
# def index
|
50
|
+
# fieldset_for :post, default: true, default_fields: [:id, :title, :author]
|
51
|
+
# fieldset_for :user, permitted_fields: [:id, :name, :posts, :avatar_url],
|
52
|
+
# defaults_to_permitted_fields: true
|
53
|
+
# # ...
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# or in the Grape method if you're using Grape:
|
45
57
|
#
|
46
58
|
# resources :posts do
|
47
59
|
# get do
|
48
|
-
# fieldset_for :post,
|
60
|
+
# fieldset_for :post, default: true, default_fields: [:id, :title, :author]
|
49
61
|
# fieldset_for :user, permitted_fields: [:id, :name, :posts, :avatar_url],
|
50
|
-
#
|
62
|
+
# defaults_to_permitted_fields: true
|
51
63
|
# # ...
|
52
64
|
# end
|
53
65
|
# end
|
54
66
|
#
|
55
|
-
#
|
56
|
-
#
|
57
|
-
#
|
67
|
+
# The +fieldset_for+ method used above parses the +fields+ and/or
|
68
|
+
# <tt>fields[resource_name]</tt> parameters, and save the results into
|
69
|
+
# +@fieldset+ instance variable for further usage.
|
58
70
|
#
|
59
|
-
# After
|
60
|
-
#
|
71
|
+
# After that line, you can use the +fieldset+ helper method to get the fieldset
|
72
|
+
# information. Actual examples are:
|
61
73
|
#
|
62
74
|
# With <tt>GET /posts?fields=title,author</tt>:
|
63
75
|
#
|
@@ -70,26 +82,53 @@ require 'active_support'
|
|
70
82
|
# fieldset(:post, :title) #=> true
|
71
83
|
# fieldset(:user, :avatar_url) #=> false
|
72
84
|
#
|
73
|
-
# You can make use of
|
85
|
+
# You can make use of these information while dealing with requests in the
|
86
|
+
# controller, for example:
|
87
|
+
#
|
88
|
+
# Post.select(fieldset(:post)).find(params[:id])
|
89
|
+
#
|
90
|
+
# And return only specified fields in the view, for instance, Jbuilder:
|
91
|
+
#
|
92
|
+
# json.(@post, *fieldset(:post))
|
93
|
+
# json.author do
|
94
|
+
# json.(@author, *fieldset(:user))
|
95
|
+
# end
|
96
|
+
#
|
97
|
+
# or RABL:
|
98
|
+
#
|
99
|
+
# # post.rabl
|
74
100
|
#
|
75
|
-
#
|
101
|
+
# object @post
|
102
|
+
# attributes(*fieldset[:post])
|
103
|
+
# child :author do
|
104
|
+
# extends 'user'
|
105
|
+
# end
|
106
|
+
#
|
107
|
+
# # user.rabl
|
108
|
+
#
|
109
|
+
# object @user
|
110
|
+
# attributes(*fieldset[:user])
|
76
111
|
#
|
77
|
-
#
|
112
|
+
# You can also set properties of fieldset with the +set_fieldset+ helper method
|
113
|
+
# in the views if you're using a same view across multiple controllers, for
|
114
|
+
# decreasing code duplication or increasing security. Below is an example with
|
115
|
+
# RABL:
|
78
116
|
#
|
79
117
|
# object @user
|
80
118
|
#
|
81
|
-
# # this ensures the +fieldset+ instance variable is least setted with
|
82
|
-
# # the default fields, and double
|
83
|
-
# # in case of things going wrong in the controller
|
119
|
+
# # this ensures that the +fieldset+ instance variable is least setted with
|
120
|
+
# # the default fields, and double filters +permitted_fields+ at view layer -
|
121
|
+
# # in case of any things going wrong in the controller
|
84
122
|
# set_fieldset :user, default_fields: [:id, :name, :avatar_url],
|
85
123
|
# permitted_fields: [:id, :name, :avatar_url, :posts]
|
86
124
|
#
|
87
125
|
# # determine the fields to show on the fly
|
88
126
|
# attributes(*fieldset[:user])
|
127
|
+
#
|
89
128
|
module APIHelper::Fieldsettable
|
90
129
|
extend ActiveSupport::Concern
|
91
130
|
|
92
|
-
# Gets the fields parameters, organize them into a +@fieldset+ hash for model to select certain
|
131
|
+
# Gets the fields parameters, organize them into a +@fieldset+ hash for model to select certain.
|
93
132
|
# fields and/or templates to render specified fieldset. Following the URL rules of JSON API:
|
94
133
|
# http://jsonapi.org/format/#fetching-sparse-fieldsets
|
95
134
|
#
|
@@ -98,8 +137,9 @@ module APIHelper::Fieldsettable
|
|
98
137
|
# +resource+::
|
99
138
|
# +Symbol+ name of resource to receive the fieldset
|
100
139
|
#
|
101
|
-
# +
|
102
|
-
# +Boolean+ should this resource take the parameter from +fields+ while no
|
140
|
+
# +default+::
|
141
|
+
# +Boolean+ should this resource take the parameter from +fields+ while no
|
142
|
+
# resourse name is specified?
|
103
143
|
#
|
104
144
|
# +permitted_fields+::
|
105
145
|
# +Array+ of +Symbol+s list of accessible fields used to filter out unpermitted fields,
|
@@ -108,9 +148,9 @@ module APIHelper::Fieldsettable
|
|
108
148
|
# +default_fields+::
|
109
149
|
# +Array+ of +Symbol+s list of fields to show by default
|
110
150
|
#
|
111
|
-
# +
|
112
|
-
# +Boolean+ if set to true, @fieldset will be set to all permitted_fields
|
113
|
-
# resource's fieldset isn't specified
|
151
|
+
# +defaults_to_permitted_fields+::
|
152
|
+
# +Boolean+ if set to true, @fieldset will be set to all permitted_fields
|
153
|
+
# when the current resource's fieldset isn't specified
|
114
154
|
#
|
115
155
|
# Example Result:
|
116
156
|
#
|
@@ -121,46 +161,88 @@ module APIHelper::Fieldsettable
|
|
121
161
|
# # :user => [:id, :name, :email, :groups],
|
122
162
|
# # :group => [:id, :name]
|
123
163
|
# # }
|
124
|
-
|
125
|
-
|
126
|
-
|
164
|
+
#
|
165
|
+
def fieldset_for(resource, default: false,
|
166
|
+
permitted_fields: [],
|
167
|
+
defaults_to_permitted_fields: false,
|
168
|
+
default_fields: [])
|
169
|
+
@fieldset ||= ActiveSupport::HashWithIndifferentAccess.new
|
127
170
|
|
128
171
|
# put the fields in place
|
129
|
-
if params[:fields].is_a?
|
172
|
+
if params[:fields].is_a?(Hash)
|
173
|
+
# get the specific resource fields from fields hash
|
130
174
|
@fieldset[resource] = params[:fields][resource] || params[:fields][resource]
|
131
|
-
elsif
|
175
|
+
elsif default
|
176
|
+
# or get the fields string directly if this resource is th default one
|
132
177
|
@fieldset[resource] = params[:fields]
|
133
178
|
end
|
134
179
|
|
135
|
-
# splits the string into array
|
136
|
-
|
180
|
+
# splits the string into array
|
181
|
+
if @fieldset[resource].present?
|
182
|
+
@fieldset[resource] = @fieldset[resource].split(',').map(&:to_s)
|
183
|
+
else
|
184
|
+
@fieldset[resource] = default_fields.map(&:to_s)
|
185
|
+
end
|
137
186
|
|
138
|
-
|
139
|
-
|
187
|
+
if permitted_fields.present?
|
188
|
+
permitted_fields = permitted_fields.map(&:to_s)
|
140
189
|
|
141
|
-
|
142
|
-
|
143
|
-
end
|
190
|
+
# filter out unpermitted fields by intersecting them
|
191
|
+
@fieldset[resource] &= permitted_fields if @fieldset[resource].present?
|
144
192
|
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
@fieldset[resource] &= permitted_fields
|
193
|
+
# set default fields to permitted_fields if needed
|
194
|
+
@fieldset[resource] = permitted_fields if @fieldset[resource].blank? &&
|
195
|
+
defaults_to_permitted_fields
|
196
|
+
end
|
150
197
|
end
|
151
198
|
|
152
199
|
# Getter for the fieldset data
|
200
|
+
#
|
201
|
+
# This method will act as a traditional getter of the fieldset data and
|
202
|
+
# returns a hash containing fields for each resource if no parameter is
|
203
|
+
# provided.
|
204
|
+
#
|
205
|
+
# fieldset # => { 'user' => ['name'], 'post' => ['title', 'author'] }
|
206
|
+
#
|
207
|
+
# If one parameter - a specific resourse name is passed in, it will return
|
208
|
+
# a fields array of that specific resourse.
|
209
|
+
#
|
210
|
+
# fieldset(:post) # => ['title', 'author']
|
211
|
+
#
|
212
|
+
# And if one more parameter - a field name, is passed in, it will return a
|
213
|
+
# boolen, determining if that field should exist in that resource.
|
214
|
+
#
|
215
|
+
# fieldset(:post, :title) # => true
|
216
|
+
#
|
153
217
|
def fieldset(resource = nil, field = nil)
|
218
|
+
# act as a traditional getter if no parameters specified
|
154
219
|
if resource.blank?
|
155
|
-
@fieldset ||=
|
220
|
+
@fieldset ||= ActiveSupport::HashWithIndifferentAccess.new
|
221
|
+
|
222
|
+
# returns the fieldset array if an specific resource is passed in
|
156
223
|
elsif field.blank?
|
157
|
-
|
224
|
+
fieldset[resource] || []
|
225
|
+
|
226
|
+
# determine if a field is inculded in a specific fieldset
|
158
227
|
else
|
159
|
-
|
228
|
+
field = field.to_s
|
229
|
+
fieldset(resource).is_a?(Array) && fieldset(resource).include?(field)
|
160
230
|
end
|
161
231
|
end
|
162
232
|
|
163
|
-
#
|
233
|
+
# View Helper to set the default and permitted fields
|
234
|
+
#
|
235
|
+
# This is useful while using an resource view shared by multiple controllers,
|
236
|
+
# it will ensure the +@fieldset+ instance variable presents, and can also set
|
237
|
+
# the default fields of a model for convenience, or the whitelisted permitted
|
238
|
+
# fields for security.
|
239
|
+
def set_fieldset(resource, default_fields: [], permitted_fields: [])
|
240
|
+
@fieldset ||= ActiveSupport::HashWithIndifferentAccess.new
|
241
|
+
@fieldset[resource] = default_fields.map(&:to_s) if @fieldset[resource].blank?
|
242
|
+
@fieldset[resource] &= permitted_fields.map(&:to_s) if permitted_fields.present?
|
243
|
+
end
|
244
|
+
|
245
|
+
# Returns the description of the 'fields' URL parameter
|
164
246
|
def self.fields_param_desc(example: nil)
|
165
247
|
if example.present?
|
166
248
|
"Choose the fields to be returned. Example value: '#{example}'"
|
@@ -168,4 +250,10 @@ module APIHelper::Fieldsettable
|
|
168
250
|
"Choose the fields to be returned."
|
169
251
|
end
|
170
252
|
end
|
253
|
+
|
254
|
+
included do
|
255
|
+
if defined? helper_method
|
256
|
+
helper_method :fieldset, :set_fieldset
|
257
|
+
end
|
258
|
+
end
|
171
259
|
end
|
@@ -1,9 +1,10 @@
|
|
1
1
|
require 'active_support'
|
2
|
+
require 'active_support/core_ext/object/blank'
|
2
3
|
|
3
|
-
# =
|
4
|
+
# = Filterable
|
4
5
|
#
|
5
|
-
# A filterable resource API supports requests to filter resources
|
6
|
-
#
|
6
|
+
# A filterable resource API supports requests to filter resources in collection
|
7
|
+
# by their fields, using the +filter+ query parameter.
|
7
8
|
#
|
8
9
|
# For example, the following is a request for all products that has a
|
9
10
|
# particular color:
|
@@ -16,19 +17,23 @@ require 'active_support'
|
|
16
17
|
#
|
17
18
|
# <em>Multiple filters are applied with the AND condition.</em>
|
18
19
|
#
|
19
|
-
#
|
20
|
+
# A list separated by commas (",") can be used to filter by field matching one
|
21
|
+
# of the values:
|
20
22
|
#
|
21
23
|
# GET /products?filter[color]=red,blue,yellow
|
22
24
|
#
|
23
25
|
# A few functions: +not+, +greater_then+, +less_then+, +greater_then_or_equal+,
|
24
|
-
# +less_then_or_equal+, +between+ and +
|
25
|
-
# the data, for example:
|
26
|
+
# +less_then_or_equal+, +between+, +like+, +contains+, +null+ and +blank+ can
|
27
|
+
# be used to filter the data, for example:
|
26
28
|
#
|
27
29
|
# GET /products?filter[color]=not(red)
|
28
30
|
# GET /products?filter[price]=greater_then(1000)
|
29
31
|
# GET /products?filter[price]=less_then_or_equal(2000)
|
30
32
|
# GET /products?filter[price]=between(1000,2000)
|
31
33
|
# GET /products?filter[name]=like(%lovely%)
|
34
|
+
# GET /products?filter[name]=contains(%lovely%)
|
35
|
+
# GET /products?filter[provider]=null()
|
36
|
+
# GET /products?filter[provider]=blank()
|
32
37
|
#
|
33
38
|
# == Usage
|
34
39
|
#
|
@@ -41,20 +46,16 @@ require 'active_support'
|
|
41
46
|
# or in your Grape API class:
|
42
47
|
#
|
43
48
|
# class SampleAPI < Grape::API
|
44
|
-
#
|
49
|
+
# helpers APIHelper::Filterable
|
45
50
|
# end
|
46
51
|
#
|
47
|
-
# then use the +filter+ method like this:
|
52
|
+
# then use the +filter+ method in the controller like this:
|
48
53
|
#
|
49
|
-
#
|
50
|
-
#
|
51
|
-
#
|
52
|
-
#
|
53
|
-
# end
|
54
|
-
# end
|
54
|
+
# @products = filter(Post, filterable_fields: [:name, :price, :color])
|
55
|
+
#
|
56
|
+
# <em>The +filter+ method will return a scoped model collection, based
|
57
|
+
# directly from the requested URL parameters.</em>
|
55
58
|
#
|
56
|
-
# <em>The +filter+ method will return the scoped model, based directly
|
57
|
-
# from the requested URL.</em>
|
58
59
|
module APIHelper::Filterable
|
59
60
|
extend ActiveSupport::Concern
|
60
61
|
|
@@ -63,20 +64,25 @@ module APIHelper::Filterable
|
|
63
64
|
# Params:
|
64
65
|
#
|
65
66
|
# +resource+::
|
66
|
-
# +ActiveRecord::
|
67
|
+
# +ActiveRecord::Relation+ resource collection
|
67
68
|
# to filter data from
|
68
69
|
#
|
69
70
|
# +filterable_fields+::
|
70
|
-
# +Array+ of +Symbol+s fields that are allowed to be filtered,
|
71
|
+
# +Array+ of +Symbol+s fields that are allowed to be filtered, defaults
|
71
72
|
# to all
|
73
|
+
#
|
72
74
|
def filter(resource, filterable_fields: [])
|
73
75
|
# parse the request parameter
|
74
|
-
if params[:filter].is_a?
|
76
|
+
if params[:filter].is_a?(Hash)
|
75
77
|
@filter = params[:filter]
|
76
78
|
filterable_fields = filterable_fields.map(&:to_s)
|
77
79
|
|
80
|
+
# deal with each condition
|
78
81
|
@filter.each_pair do |field, condition|
|
82
|
+
# bypass fields that aren't be abled to filter with
|
79
83
|
next if filterable_fields.present? && !filterable_fields.include?(field)
|
84
|
+
|
85
|
+
# escape string to prevent SQL injection
|
80
86
|
field = resource.connection.quote_string(field)
|
81
87
|
|
82
88
|
next if resource.columns_hash[field].blank?
|
@@ -89,22 +95,50 @@ module APIHelper::Filterable
|
|
89
95
|
values = func[:param].split(',')
|
90
96
|
values.map!(&:to_bool) if field_type == :boolean
|
91
97
|
resource = resource.where.not(field => values)
|
98
|
+
|
92
99
|
when 'greater_then'
|
93
|
-
resource = resource
|
100
|
+
resource = resource
|
101
|
+
.where("\"#{resource.table_name}\".\"#{field}\" > ?",
|
102
|
+
func[:param])
|
103
|
+
|
94
104
|
when 'less_then'
|
95
|
-
resource = resource
|
105
|
+
resource = resource
|
106
|
+
.where("\"#{resource.table_name}\".\"#{field}\" < ?",
|
107
|
+
func[:param])
|
108
|
+
|
96
109
|
when 'greater_then_or_equal'
|
97
|
-
resource = resource
|
110
|
+
resource = resource
|
111
|
+
.where("\"#{resource.table_name}\".\"#{field}\" >= ?",
|
112
|
+
func[:param])
|
113
|
+
|
98
114
|
when 'less_then_or_equal'
|
99
|
-
resource = resource
|
115
|
+
resource = resource
|
116
|
+
.where("\"#{resource.table_name}\".\"#{field}\" <= ?",
|
117
|
+
func[:param])
|
118
|
+
|
100
119
|
when 'between'
|
101
120
|
param = func[:param].split(',')
|
102
|
-
resource = resource
|
121
|
+
resource = resource
|
122
|
+
.where("\"#{resource.table_name}\".\"#{field}\" BETWEEN ? AND ?",
|
123
|
+
param.first, param.last)
|
124
|
+
|
103
125
|
when 'like'
|
104
|
-
resource = resource
|
126
|
+
resource = resource
|
127
|
+
.where("\"#{resource.table_name}\".\"#{field}\" LIKE ?",
|
128
|
+
func[:param])
|
129
|
+
|
130
|
+
when 'contains'
|
131
|
+
resource = resource
|
132
|
+
.where("\"#{resource.table_name}\".\"#{field}\" LIKE ?",
|
133
|
+
"%#{func[:param]}%")
|
134
|
+
|
105
135
|
when 'null'
|
106
136
|
resource = resource.where(field => nil)
|
137
|
+
|
138
|
+
when 'blank'
|
139
|
+
resource = resource.where(field => [nil, ''])
|
107
140
|
end
|
141
|
+
|
108
142
|
# if not function
|
109
143
|
else
|
110
144
|
values = condition.split(',')
|
@@ -117,7 +151,7 @@ module APIHelper::Filterable
|
|
117
151
|
return resource
|
118
152
|
end
|
119
153
|
|
120
|
-
#
|
154
|
+
# Returns a description of the 'fields' URL parameter
|
121
155
|
def self.filter_param_desc(for_field: nil)
|
122
156
|
if for_field.present?
|
123
157
|
"Filter data base on the '#{for_field}' field."
|
@@ -126,3 +160,9 @@ module APIHelper::Filterable
|
|
126
160
|
end
|
127
161
|
end
|
128
162
|
end
|
163
|
+
|
164
|
+
class String
|
165
|
+
def to_bool
|
166
|
+
self == 'true'
|
167
|
+
end
|
168
|
+
end
|
@@ -1,16 +1,16 @@
|
|
1
1
|
require 'active_support'
|
2
2
|
|
3
|
-
# =
|
3
|
+
# = Includable
|
4
4
|
#
|
5
|
-
# Inclusion
|
6
|
-
#
|
7
|
-
#
|
5
|
+
# Inclusion lets your API returns not only the data of the primary resource,
|
6
|
+
# but also resources that have relation to it. Includable APIs will also
|
7
|
+
# support customising the resources included using the +include+ parameter.
|
8
8
|
#
|
9
9
|
# This design made references to the rules of <em>Inclusion of Related
|
10
10
|
# Resources</em> in <em>JSON API</em>:
|
11
11
|
# http://jsonapi.org/format/#fetching-includes
|
12
12
|
#
|
13
|
-
# For instance,
|
13
|
+
# For instance, articles can be requested with their comments along:
|
14
14
|
#
|
15
15
|
# GET /articles?include=comments
|
16
16
|
#
|
@@ -57,7 +57,7 @@ require 'active_support'
|
|
57
57
|
# }
|
58
58
|
# ]
|
59
59
|
#
|
60
|
-
# instead of just
|
60
|
+
# instead of just the ids of each comment
|
61
61
|
#
|
62
62
|
# [
|
63
63
|
# {
|
@@ -74,8 +74,8 @@ require 'active_support'
|
|
74
74
|
# }
|
75
75
|
# ]
|
76
76
|
#
|
77
|
-
#
|
78
|
-
#
|
77
|
+
# Multiple related resources can be stated in a comma-separated list,
|
78
|
+
# like this:
|
79
79
|
#
|
80
80
|
# GET /articles/12?include=author,comments
|
81
81
|
#
|
@@ -90,118 +90,243 @@ require 'active_support'
|
|
90
90
|
# or in your Grape API class:
|
91
91
|
#
|
92
92
|
# class SampleAPI < Grape::API
|
93
|
-
#
|
93
|
+
# helpers APIHelper::Includable
|
94
|
+
# end
|
95
|
+
#
|
96
|
+
# Then setup inclusion with +inclusion_for+ in the controller:
|
97
|
+
#
|
98
|
+
# def index
|
99
|
+
# inclusion_for :post, default: true
|
100
|
+
# # ...
|
94
101
|
# end
|
95
102
|
#
|
96
|
-
#
|
103
|
+
# or in the Grape method if you're using it:
|
97
104
|
#
|
98
105
|
# resources :posts do
|
99
106
|
# get do
|
100
|
-
# inclusion_for :post,
|
107
|
+
# inclusion_for :post, default: true
|
101
108
|
# # ...
|
102
109
|
# end
|
103
110
|
# end
|
104
111
|
#
|
105
|
-
# This helper parses the +include+ and <tt>include[
|
106
|
-
#
|
107
|
-
#
|
112
|
+
# This helper parses the +include+ and/or <tt>include[resource_name]</tt>
|
113
|
+
# parameters and saves the results into +@inclusion+ for further usage.
|
114
|
+
#
|
115
|
+
# +Includable+ integrates with +Fieldsettable+ if used together, by:
|
116
|
+
#
|
117
|
+
# * Sliceing the included fields that dosen't appears in the fieldset - since
|
118
|
+
# the included resoure(s) are actually fields under the primary resorce,
|
119
|
+
# fieldset will be in charged to determine the fields to show. Thus, fields
|
120
|
+
# will be totally ignored if they aren't appeared in the fieldset, regardless
|
121
|
+
# if they are included or not.
|
122
|
+
#
|
123
|
+
# So notice that +inclusion_for+ should be set after +fieldset_for+ if both are
|
124
|
+
# used!
|
108
125
|
#
|
109
|
-
# After
|
110
|
-
#
|
126
|
+
# After that +inclusion_for ...+ line, you can use the +inclusion+ helper
|
127
|
+
# method to get the inclusion data of each request, and do something like this
|
128
|
+
# in your controller:
|
111
129
|
#
|
112
|
-
#
|
130
|
+
# @posts = Post.includes(inclusion(:post))
|
113
131
|
#
|
114
|
-
# The +inclusion+ helper method
|
132
|
+
# The +inclusion+ helper method will return data depending on the parameters
|
133
|
+
# passed in, as the following example:
|
115
134
|
#
|
116
|
-
# inclusion
|
117
|
-
# inclusion(:post)
|
118
|
-
# inclusion(:post, :author)
|
135
|
+
# inclusion # => { 'post' => ['author'] }
|
136
|
+
# inclusion(:post) # => ['author']
|
137
|
+
# inclusion(:post, :author) # => true
|
138
|
+
#
|
139
|
+
# And don't forget to set your API views or serializers with the help of
|
140
|
+
# +inclusion+ to provide dynamic included resources!
|
119
141
|
#
|
120
142
|
# === API View with RABL
|
121
143
|
#
|
122
144
|
# If you're using RABL as the API view, it can be setup like this:
|
123
145
|
#
|
146
|
+
# object @post
|
147
|
+
#
|
124
148
|
# # set the includable and default inclusion fields of the view
|
125
149
|
# set_inclusion :post, default_includes: [:author]
|
126
150
|
#
|
127
151
|
# # set the details for all includable fields
|
128
152
|
# set_inclusion_field :post, :author, :author_id
|
153
|
+
# set_inclusion_field :post, :comments, :comment_ids
|
129
154
|
#
|
130
155
|
# # extends the partial to show included fields
|
131
156
|
# extends('extensions/includable_childs', locals: { self_resource: :post })
|
157
|
+
#
|
158
|
+
# --
|
159
|
+
# TODO: provide an example of includable_childs.rabl
|
160
|
+
# ++
|
161
|
+
#
|
132
162
|
module APIHelper::Includable
|
133
163
|
extend ActiveSupport::Concern
|
134
164
|
|
135
|
-
# Gets the include parameters, organize them into a +@inclusion+ hash
|
136
|
-
# inner-join queries and/or templates to render relation attributes included.
|
137
|
-
# Following the URL rules of JSON API:
|
138
|
-
# http://jsonapi.org/format/#fetching-includes
|
165
|
+
# Gets the include parameters, organize them into a +@inclusion+ hash.
|
139
166
|
#
|
140
167
|
# Params:
|
141
168
|
#
|
142
169
|
# +resource+::
|
143
170
|
# +Symbol+ name of resource to receive the inclusion
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
171
|
+
#
|
172
|
+
# +default+::
|
173
|
+
# +Boolean+ should this resource take the parameter from +include+ while no
|
174
|
+
# resourse name is specified?
|
175
|
+
#
|
176
|
+
# +permitted_includes+::
|
177
|
+
# +Array+ of +Symbol+s list of includable fields, permitting all by default
|
178
|
+
#
|
179
|
+
# +default_includes+::
|
180
|
+
# +Array+ of +Symbol+s list of fields to be included by default
|
181
|
+
#
|
182
|
+
# +defaults_to_permitted_includes+::
|
183
|
+
# +Boolean+ if set to true, +@inclusion+ will be set to all
|
184
|
+
# permitted_includes when the current resource's included fields
|
185
|
+
# isn't specified
|
186
|
+
#
|
187
|
+
def inclusion_for(resource, default: false,
|
188
|
+
permitted_includes: [],
|
189
|
+
defaults_to_permitted_includes: false,
|
190
|
+
default_includes: [])
|
191
|
+
@inclusion ||= ActiveSupport::HashWithIndifferentAccess.new
|
192
|
+
@inclusion_specified ||= ActiveSupport::HashWithIndifferentAccess.new
|
193
|
+
|
194
|
+
# put the fields in place
|
195
|
+
if params[:include].is_a?(Hash)
|
196
|
+
# get the specific resource inclusion fields from the "include" hash
|
197
|
+
@inclusion[resource] = params[:include][resource]
|
198
|
+
@inclusion_specified[resource] = true if params[:include][resource].present?
|
199
|
+
elsif default
|
200
|
+
# or get the "include" string directly if this resource is th default one
|
152
201
|
@inclusion[resource] = params[:include]
|
202
|
+
@inclusion_specified[resource] = true if params[:include].present?
|
153
203
|
end
|
154
204
|
|
155
|
-
# splits the string into array
|
156
|
-
|
157
|
-
|
205
|
+
# splits the string into array
|
206
|
+
if @inclusion[resource].present?
|
207
|
+
@inclusion[resource] = @inclusion[resource].split(',').map(&:to_s)
|
208
|
+
elsif !@inclusion_specified[resource]
|
209
|
+
@inclusion[resource] = default_includes.map(&:to_s)
|
210
|
+
end
|
158
211
|
|
159
|
-
|
160
|
-
|
161
|
-
@inclusion ||= {}
|
162
|
-
@inclusion_field ||= {}
|
163
|
-
@inclusion[resource] = default_includes if @inclusion[resource].blank?
|
164
|
-
end
|
212
|
+
if permitted_includes.present?
|
213
|
+
permitted_includes = permitted_includes.map(&:to_s)
|
165
214
|
|
166
|
-
|
167
|
-
|
168
|
-
return if (@fieldset.present? && @fieldset[self_resource].present? && !@fieldset[self_resource].include?(field))
|
215
|
+
# filter out unpermitted includes by intersecting them
|
216
|
+
@inclusion[resource] &= permitted_includes if @inclusion[resource].present?
|
169
217
|
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
@fieldset[self_resource].delete(field) if @fieldset[self_resource].present?
|
218
|
+
# set default inclusion to permitted_includes if needed
|
219
|
+
@inclusion[resource] = permitted_includes if @inclusion[resource].blank? &&
|
220
|
+
defaults_to_permitted_includes &&
|
221
|
+
!@inclusion_specified[resource]
|
222
|
+
end
|
223
|
+
|
224
|
+
if @fieldset.is_a?(Hash) && @fieldset[resource].present?
|
225
|
+
@inclusion[resource] &= @fieldset[resource]
|
226
|
+
end
|
180
227
|
end
|
181
228
|
|
182
229
|
# Getter for the inclusion data.
|
230
|
+
#
|
231
|
+
# This method will act as a traditional getter of the inclusion data and
|
232
|
+
# returns a hash containing fields for each resource if no parameter is
|
233
|
+
# provided.
|
234
|
+
#
|
235
|
+
# inclusion # => { 'post' => ['author', 'comments'] }
|
236
|
+
#
|
237
|
+
# If one parameter - a specific resourse name is passed in, it will return an
|
238
|
+
# array of relation names that should be included for that specific resourse.
|
239
|
+
#
|
240
|
+
# inclusion(:post) # => ['author', 'comments']
|
241
|
+
#
|
242
|
+
# And if one more parameter - a field name, is passed in, it will return a
|
243
|
+
# boolen, determining if that relation should be included in the response.
|
244
|
+
#
|
245
|
+
# inclusion(:post, :author) # => true
|
246
|
+
#
|
183
247
|
def inclusion(resource = nil, field = nil)
|
248
|
+
# act as a traditional getter if no parameters specified
|
184
249
|
if resource.blank?
|
185
|
-
@inclusion ||=
|
250
|
+
@inclusion ||= ActiveSupport::HashWithIndifferentAccess.new
|
251
|
+
|
252
|
+
# returns the inclusion array if an specific resource is passed in
|
186
253
|
elsif field.blank?
|
187
|
-
|
254
|
+
inclusion[resource] || []
|
255
|
+
|
256
|
+
# determine if a field is inculded
|
188
257
|
else
|
189
|
-
|
190
|
-
inclusion(resource).include?(field)
|
258
|
+
field = field.to_s
|
259
|
+
inclusion(resource).is_a?(Array) && inclusion(resource).include?(field)
|
191
260
|
end
|
192
261
|
end
|
193
262
|
|
194
|
-
#
|
263
|
+
# View Helper to set the inclusion
|
264
|
+
#
|
265
|
+
# This is useful while using an resource view shared by multiple controllers,
|
266
|
+
# this will ensure the +@inclusion+ instance variable presents, and can also
|
267
|
+
# set the default included fields of a model for convenience, or the fields
|
268
|
+
# that are permitted to be included for security.
|
269
|
+
def set_inclusion(resource, default_includes: [], permitted_includes: [])
|
270
|
+
@inclusion ||= ActiveSupport::HashWithIndifferentAccess.new
|
271
|
+
@inclusion_field ||= ActiveSupport::HashWithIndifferentAccess.new
|
272
|
+
@inclusion[resource] = default_includes.map(&:to_s) if @inclusion[resource].blank? &&
|
273
|
+
!@inclusion_specified[resource]
|
274
|
+
@inclusion[resource] &= permitted_includes.map(&:to_s) if permitted_includes.present?
|
275
|
+
end
|
276
|
+
|
277
|
+
# View Helper to set the inclusion details
|
278
|
+
#
|
279
|
+
# Params:
|
280
|
+
#
|
281
|
+
# +resource+::
|
282
|
+
# +Symbol+ name of the resource to receive the inclusion field data
|
283
|
+
#
|
284
|
+
# +field+::
|
285
|
+
# +Symbol+ the field name of the relatiion that can be included
|
286
|
+
#
|
287
|
+
# +id_field+::
|
288
|
+
# +Symbol+ the field to use (normally suffixed with "_id") if the object
|
289
|
+
# isn't included
|
290
|
+
#
|
291
|
+
# +resource_name+::
|
292
|
+
# +Symbol+ the name of the child resource, can be used to determine which
|
293
|
+
# view template should be extended for rendering that child node and also
|
294
|
+
# can shown in the response metadata as well
|
295
|
+
#
|
296
|
+
# +resources_url+::
|
297
|
+
# +String+ the resources URL of the child resource, can be used to be shown
|
298
|
+
# in the metadata for the clients' convenience to learn ablou the API
|
299
|
+
#
|
300
|
+
def set_inclusion_field(resource, field, id_field, resource_name: nil,
|
301
|
+
resources_url: nil)
|
302
|
+
@inclusion_field ||= ActiveSupport::HashWithIndifferentAccess.new
|
303
|
+
@inclusion_field[resource] ||= ActiveSupport::HashWithIndifferentAccess.new
|
304
|
+
@inclusion_field[resource][field] = {
|
305
|
+
field: field,
|
306
|
+
id_field: id_field,
|
307
|
+
resource_name: resource_name,
|
308
|
+
resources_url: resources_url
|
309
|
+
}
|
310
|
+
end
|
311
|
+
|
312
|
+
# Returns the description of the 'include' URL parameter
|
195
313
|
def self.include_param_desc(example: nil, default: nil)
|
196
314
|
if default.present?
|
197
315
|
desc = "Returning compound documents that include specific associated objects, defaults to '#{default}'."
|
198
316
|
else
|
199
317
|
desc = "Returning compound documents that include specific associated objects."
|
200
318
|
end
|
319
|
+
|
201
320
|
if example.present?
|
202
321
|
"#{desc} Example value: '#{example}'"
|
203
322
|
else
|
204
323
|
desc
|
205
324
|
end
|
206
325
|
end
|
326
|
+
|
327
|
+
included do
|
328
|
+
if defined? helper_method
|
329
|
+
helper_method :inclusion, :set_inclusion, :set_inclusion_field
|
330
|
+
end
|
331
|
+
end
|
207
332
|
end
|
data/lib/api_helper/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: api_helper
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Neson
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-06-
|
11
|
+
date: 2015-06-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: appraisal
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: rspec
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -52,6 +66,48 @@ dependencies:
|
|
52
66
|
- - ">="
|
53
67
|
- !ruby/object:Gem::Version
|
54
68
|
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: byebug
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: activerecord
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: sqlite3
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
55
111
|
- !ruby/object:Gem::Dependency
|
56
112
|
name: activesupport
|
57
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -76,12 +132,20 @@ files:
|
|
76
132
|
- ".gitignore"
|
77
133
|
- ".rspec"
|
78
134
|
- ".travis.yml"
|
135
|
+
- Appraisals
|
79
136
|
- Gemfile
|
80
137
|
- README.md
|
81
138
|
- Rakefile
|
82
139
|
- api_helper.gemspec
|
83
140
|
- bin/console
|
84
141
|
- bin/setup
|
142
|
+
- examples/includable_childs.rabl
|
143
|
+
- gemfiles/grape_0.10.0.gemfile
|
144
|
+
- gemfiles/grape_0.11.0.gemfile
|
145
|
+
- gemfiles/rails_4.0.0.gemfile
|
146
|
+
- gemfiles/rails_4.1.0.gemfile
|
147
|
+
- gemfiles/rails_4.1.8.gemfile
|
148
|
+
- gemfiles/rails_4.2.0.gemfile
|
85
149
|
- lib/api_helper.rb
|
86
150
|
- lib/api_helper/fieldsettable.rb
|
87
151
|
- lib/api_helper/filterable.rb
|
@@ -114,3 +178,4 @@ signing_key:
|
|
114
178
|
specification_version: 4
|
115
179
|
summary: Helpers for creating standard web API.
|
116
180
|
test_files: []
|
181
|
+
has_rdoc:
|