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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ea6fee61c677b49c8f59a3cba0c0d7bcdfb570ef
4
- data.tar.gz: 2f2a2f2960c5e406a9c2a5c8a52f813bd13adfad
3
+ metadata.gz: 326e1e9e2c1b9509990e1103a0d8f95da5be83a9
4
+ data.tar.gz: 4c9fd145828a49a586d8a4b7dee67fa177ea957c
5
5
  SHA512:
6
- metadata.gz: f6fb52e869d6d29c3aabd48dc69f50a30d1a869b882a6416d6abeca01ebbfbbae56e9959a553bc54d9e38f1c08904c3dce3d068b5f18c3744202bc7a49ff23dc
7
- data.tar.gz: 1b02bc8d125ce5c0091bb8c2a2fa8c3519eab5598d2a497b2f5cc814d78696b9f88e619b8533433d66eda6544234df71352d2b0ee33e6a1d8f35c1af7ff19363
6
+ metadata.gz: d14e69fb29488afe354b941fc4501ba92620936731673a2bc41274cf37f77bd4f2fbcf7e368e9dd8cc52e473a4b9b8ce1fd1b49391d59f6e638df0fac6452eda
7
+ data.tar.gz: f44a72e00cc934ad01194d0c20fa799f0fc6e232d26abced40f6a60b120b6942b66db0d3e21bfa90496ecc10e0b51b84833ad77727ebb2b85363dd1a5c7e5177
data/.gitignore CHANGED
@@ -7,3 +7,4 @@
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /tmp/
10
+ /gemfiles/*.gemfile.lock
data/.travis.yml CHANGED
@@ -1,4 +1,10 @@
1
1
  language: ruby
2
2
  rvm:
3
+ - 2.0.0
3
4
  - 2.2.0
5
+ gemfile:
6
+ - gemfiles/rails_4.1.0.gemfile
7
+ - gemfiles/rails_4.2.0.gemfile
8
+ - gemfiles/grape_0.10.0.gemfile
9
+ - gemfiles/grape_0.11.0.gemfile
4
10
  before_install: gem install bundler -v 1.10.2
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 'APIHelper'
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 APIHelper
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 rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
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/APIHelper.
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
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "grape", "0.10.0"
6
+ gem "rack-test"
7
+
8
+ gemspec :path => "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "grape", "0.11.0"
6
+ gem "rack-test"
7
+
8
+ gemspec :path => "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "4.0.0"
6
+ gem "rspec-rails"
7
+
8
+ gemspec :path => "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "4.1.0"
6
+ gem "rspec-rails"
7
+
8
+ gemspec :path => "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "4.2.0"
6
+ gem "rspec-rails"
7
+
8
+ gemspec :path => "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "4.2.0"
6
+ gem "rspec-rails"
7
+
8
+ gemspec :path => "../"
@@ -1,31 +1,34 @@
1
1
  require 'active_support'
2
+ require 'active_support/core_ext/object/blank'
2
3
 
3
- # = Helper To Make Resource APIs Fieldsettable
4
+ # = Fieldsettable
4
5
  #
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.
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 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.
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 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.
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 object as well.
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
- # include APIHelper::Fieldsettable
44
+ # helpers APIHelper::Fieldsettable
42
45
  # end
43
46
  #
44
- # then set the options for the fieldset in the grape method:
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, root: true, default_fields: [:id, :title, :author]
60
+ # fieldset_for :post, default: true, default_fields: [:id, :title, :author]
49
61
  # fieldset_for :user, permitted_fields: [:id, :name, :posts, :avatar_url],
50
- # show_all_permitted_fields_by_default: true
62
+ # defaults_to_permitted_fields: true
51
63
  # # ...
52
64
  # end
53
65
  # end
54
66
  #
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.
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 this you can use the +fieldset+ helper method to get the fieldset data
60
- # that the request specifies.
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 the information while dealing with requests, for example:
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
- # Post.select(fieldset(:post))...
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
- # If you're using RABL as the API view, it can be also setup like this:
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 check +permitted_fields+ at view layer -
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
- # +root+::
102
- # +Boolean+ should this resource take the parameter from +fields+ while no type is specified
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
- # +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
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
- 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
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? Hash
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 root
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 of symbles
136
- @fieldset[resource] = @fieldset[resource].present? ? @fieldset[resource].split(',').map(&:to_sym) : default_fields
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
- # filter out unpermitted fields by intersecting them
139
- @fieldset[resource] &= permitted_fields if @fieldset[resource].present? && permitted_fields.present?
187
+ if permitted_fields.present?
188
+ permitted_fields = permitted_fields.map(&:to_s)
140
189
 
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
190
+ # filter out unpermitted fields by intersecting them
191
+ @fieldset[resource] &= permitted_fields if @fieldset[resource].present?
144
192
 
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
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
- (@fieldset ||= {})[resource] ||= []
224
+ fieldset[resource] || []
225
+
226
+ # determine if a field is inculded in a specific fieldset
158
227
  else
159
- fieldset(resource).include?(field)
228
+ field = field.to_s
229
+ fieldset(resource).is_a?(Array) && fieldset(resource).include?(field)
160
230
  end
161
231
  end
162
232
 
163
- # Return the 'fields' param description
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
- # = Helper To Make Resource APIs Filterable
4
+ # = Filterable
4
5
  #
5
- # A filterable resource API supports requests to filter resources according to
6
- # specific criteria, using the +filter+ query parameter.
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
- # OR conditions of a single value can be represented as:
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 +like+ can be used while filtering
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
- # include APIHelper::Filterable
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
- # resources :products do
50
- # get do
51
- # @products = filter(Post, filterable_fields: [:name, :price, :color])
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::Base+ or +ActiveRecord::Relation+ resource collection
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, default
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? Hash
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.where("\"#{resource.table_name}\".\"#{field}\" > ?", func[:param])
100
+ resource = resource
101
+ .where("\"#{resource.table_name}\".\"#{field}\" > ?",
102
+ func[:param])
103
+
94
104
  when 'less_then'
95
- resource = resource.where("\"#{resource.table_name}\".\"#{field}\" < ?", func[:param])
105
+ resource = resource
106
+ .where("\"#{resource.table_name}\".\"#{field}\" < ?",
107
+ func[:param])
108
+
96
109
  when 'greater_then_or_equal'
97
- resource = resource.where("\"#{resource.table_name}\".\"#{field}\" >= ?", func[:param])
110
+ resource = resource
111
+ .where("\"#{resource.table_name}\".\"#{field}\" >= ?",
112
+ func[:param])
113
+
98
114
  when 'less_then_or_equal'
99
- resource = resource.where("\"#{resource.table_name}\".\"#{field}\" <= ?", func[:param])
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.where("\"#{resource.table_name}\".\"#{field}\" BETWEEN ? AND ?", param.first, param.last)
121
+ resource = resource
122
+ .where("\"#{resource.table_name}\".\"#{field}\" BETWEEN ? AND ?",
123
+ param.first, param.last)
124
+
103
125
  when 'like'
104
- resource = resource.where("\"#{resource.table_name}\".\"#{field}\" LIKE ?", func[:param])
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
- # Return the 'fields' param description
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
- # = Helper To Make Resource APIs Includable
3
+ # = Includable
4
4
  #
5
- # Inclusion of related resource lets your API return resources related to the
6
- # primary data. This endpoint will support an +include+ request parameter to
7
- # allow the client to customize which related resources should be returned.
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, comments could be requested with articles:
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
- # If requesting multiple related resources is needed, they can be stated in a
78
- # comma-separated list:
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
- # include APIHelper::Includable
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
- # then set the options for the inclusion in the grape method:
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, root: true
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[object_type]</tt> parameters to
106
- # determine what the API caller wants, and save the results into instance
107
- # variables for further usage.
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 this you can use the +inclusion+ helper method to get the inclusion data
110
- # that the request specifies, and do something like this in your controller:
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
- # resource = resource.includes(:author) if inclusion(:post, :author)
130
+ # @posts = Post.includes(inclusion(:post))
113
131
  #
114
- # The +inclusion+ helper method returns data like this:
132
+ # The +inclusion+ helper method will return data depending on the parameters
133
+ # passed in, as the following example:
115
134
  #
116
- # inclusion #=> { post: [:author] }
117
- # inclusion(:post) #=> [:author]
118
- # inclusion(:post, :author) #=> true
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 for model to use
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
- def inclusion_for(resource, root: false, default_includes: [])
145
- @inclusion ||= Hashie::Mash.new
146
- @meta ||= Hashie::Mash.new
147
-
148
- # put the includes in place
149
- if params[:include].is_a? Hash
150
- @inclusion[resource] = params[:include][resource] || params[:include][resource]
151
- elsif root
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 of symbles
156
- @inclusion[resource] = @inclusion[resource] ? @inclusion[resource].split(',').map(&:to_sym) : default_includes
157
- end
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
- # View Helper to set the inclusion and default_inclusion.
160
- def set_inclusion(resource, default_includes: [])
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
- # View Helper to set the inclusion details.
167
- def set_inclusion_field(self_resource, field, id_field, class_name: nil, url: nil)
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
- @inclusion_field ||= {}
171
- @inclusion_field[self_resource] ||= []
172
- field_data = {
173
- field: field,
174
- id_field: id_field,
175
- class_name: class_name,
176
- url: url
177
- }
178
- @inclusion_field[self_resource] << field_data
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
- (@inclusion ||= {})[resource] ||= []
254
+ inclusion[resource] || []
255
+
256
+ # determine if a field is inculded
188
257
  else
189
- return false if (try(:fieldset, resource).present? && !fieldset(resource, field))
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
- # Return the 'include' param description
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
@@ -1,3 +1,3 @@
1
1
  module APIHelper
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.3"
3
3
  end
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.2
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-12 00:00:00.000000000 Z
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: