api_helper 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 [](http://badge.fury.io/rb/api_helper) [](https://travis-ci.org/Neson/api_helper) [](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:
|