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 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: