├── .gitignore
├── .rspec
├── .travis.yml
├── Appraisals
├── Gemfile
├── README.md
├── Rakefile
├── api_helper.gemspec
├── bin
├── console
└── setup
├── examples
└── includable_childs.rabl
├── gemfiles
├── grape_0.10.0.gemfile
├── grape_0.11.0.gemfile
├── rails_4.0.0.gemfile
├── rails_4.1.0.gemfile
├── rails_4.1.8.gemfile
├── rails_4.2.0.gemfile
└── rails_5.0.0.gemfile
├── lib
├── api_helper.rb
└── api_helper
│ ├── fieldsettable.rb
│ ├── filterable.rb
│ ├── includable.rb
│ ├── multigettable.rb
│ ├── paginatable.rb
│ ├── sortable.rb
│ └── version.rb
└── spec
├── api_helper
├── fieldsettable_spec.rb
├── filterable_spec.rb
├── grape
│ ├── fieldsettable_spec.rb
│ ├── filterable_spec.rb
│ ├── includable_spec.rb
│ ├── multigettable_spec.rb
│ ├── paginatable_spec.rb
│ └── sortable_spec.rb
├── includable_spec.rb
├── multigettable_spec.rb
├── paginatable_spec.rb
├── rails
│ ├── fieldsettable_spec.rb
│ ├── filterable_spec.rb
│ ├── includable_spec.rb
│ ├── multigettable_spec.rb
│ ├── paginatable_spec.rb
│ └── sortable_spec.rb
└── sortable_spec.rb
├── api_helper_spec.rb
├── grape_helper.rb
├── rails_helper.rb
├── shared_context
└── .keep
├── spec_helper.rb
└── support
└── active_record.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /.yardoc
3 | /Gemfile.lock
4 | /_yardoc/
5 | /coverage/
6 | /doc/
7 | /pkg/
8 | /spec/reports/
9 | /tmp/
10 | /gemfiles/*.gemfile.lock
11 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --format documentation
2 | --color
3 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | rvm:
3 | - 2.3.0
4 | gemfile:
5 | - gemfiles/rails_4.1.0.gemfile
6 | - gemfiles/rails_4.2.0.gemfile
7 | - gemfiles/rails_5.0.0.gemfile
8 | - gemfiles/grape_0.10.0.gemfile
9 | - gemfiles/grape_0.11.0.gemfile
10 | before_install: gem install bundler -v 1.10.2
11 |
--------------------------------------------------------------------------------
/Appraisals:
--------------------------------------------------------------------------------
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 'rails-5.0.0' do
14 | gemspec
15 | gem 'rails', '5.0.0.beta2'
16 | gem 'rspec-rails'
17 | end
18 |
19 | appraise 'grape-0.10.0' do
20 | gemspec
21 | gem 'grape', '0.10.0'
22 | gem 'rack-test'
23 | end
24 |
25 | appraise 'grape-0.11.0' do
26 | gemspec
27 | gem 'grape', '0.11.0'
28 | gem 'rack-test'
29 | end
30 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | # Specify your gem's dependencies in api_helper.gemspec
4 | gemspec
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # APIHelper [](http://badge.fury.io/rb/api_helper) [](https://travis-ci.org/Neson/api_helper) [](https://coveralls.io/r/Neson/api_helper?branch=master) [](https://inch-ci.org/github/Neson/api_helper)
2 |
3 | Helpers for creating standard RESTful API for Rails or Grape with Active Record.
4 |
5 |
6 | ## API Standards
7 |
8 |
9 |
10 | - Fieldsettable
11 | - Let clients choose the fields they wanted to be returned with the
fields
query parameter, making their API calls optimizable to gain efficiency and speed.
12 |
13 | - Includable
14 | - Clients can use the
include
query parameter to enable inclusion of related items - for instance, get the author's data along with a post.
15 |
16 | - Paginatable
17 | - Paginate the results of a resource collection, client can get a specific page with the
page
query parameter and set a custom page size with the "per_page" query parameter.
18 |
19 | - Sortable
20 | - Client can set custom sorting with the
sort
query parameter while getting a resource collection.
21 |
22 | - Filterable
23 | - Enables clients to filter through a resource collection with their fields with the
filter
query parameter.
24 |
25 | - Multigettable
26 | - Let Client execute operations on multiple resources with a single request.
27 |
28 |
29 |
30 |
31 | ## Installation
32 |
33 | Add this line to your application's Gemfile:
34 |
35 | ```ruby
36 | gem 'api_helper'
37 | ```
38 |
39 | And then execute:
40 |
41 | $ bundle
42 |
43 |
44 | ## Usage
45 |
46 | ### Ruby on Rails (Action Pack)
47 |
48 | Include each helper concern you need in an `ActionController::Base`:
49 |
50 | ```ruby
51 | PostsController < ApplicationController
52 | include APIHelper::Filterable
53 | include APIHelper::Paginatable
54 | include APIHelper::Sortable
55 |
56 | # ...
57 |
58 | end
59 | ```
60 |
61 | Further usage of each helper can be found in the [docs](http://www.rubydoc.info/github/zetavg/api_helper/master/APIHelper).
62 |
63 | ### Grape
64 |
65 | Set the helpers you need in an `Grape::API`:
66 |
67 | ```ruby
68 | class PostsAPI < Grape::API
69 | helpers APIHelper::Filterable
70 | helpers APIHelper::Paginatable
71 | helpers APIHelper::Sortable
72 |
73 | # ...
74 |
75 | end
76 | ```
77 |
78 | Further usage of each helper can be found in the [docs](http://www.rubydoc.info/github/zetavg/api_helper/master/APIHelper).
79 |
80 |
81 | ## Development
82 |
83 | 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.
84 |
85 | 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).
86 |
87 |
88 | ## Contributing
89 |
90 | Bug reports and pull requests are welcome on GitHub at https://github.com/Neson/api_helper.
91 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/gem_tasks"
2 | require "rspec/core/rake_task"
3 |
4 | RSpec::Core::RakeTask.new(:spec)
5 |
6 | task :default => :spec
7 |
--------------------------------------------------------------------------------
/api_helper.gemspec:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | lib = File.expand_path('../lib', __FILE__)
3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4 | require 'api_helper/version'
5 |
6 | Gem::Specification.new do |spec|
7 | spec.name = "api_helper"
8 | spec.version = APIHelper::VERSION
9 | spec.authors = ["Neson"]
10 | spec.email = ["neson@dex.tw"]
11 |
12 | spec.summary = %q{Helpers for creating standard web API.}
13 | spec.description = %q{Helpers for creating standard web API for Rails or Grape with ActiveRecord.}
14 | spec.homepage = "https://github.com/Neson/APIHelper"
15 |
16 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17 | spec.bindir = "exe"
18 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19 | spec.require_paths = ["lib"]
20 |
21 | spec.add_development_dependency "bundler"
22 | spec.add_development_dependency "rake"
23 | spec.add_development_dependency "appraisal"
24 | spec.add_development_dependency "rspec"
25 | spec.add_development_dependency "simplecov"
26 | spec.add_development_dependency "coveralls"
27 | spec.add_development_dependency "byebug"
28 | spec.add_development_dependency "activerecord"
29 | spec.add_development_dependency "sqlite3"
30 |
31 | spec.add_dependency "activesupport", ">= 3"
32 | end
33 |
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require "bundler/setup"
4 | require "api_helper"
5 |
6 | # You can add fixtures and/or initialization code here to make experimenting
7 | # with your gem easier. You can also use a different console, if you like.
8 |
9 | # (If you use this, don't forget to add pry to your Gemfile!)
10 | # require "pry"
11 | # Pry.start
12 |
13 | require "irb"
14 | IRB.start
15 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -euo pipefail
3 | IFS=$'\n\t'
4 |
5 | bundle install
6 | appraisal install
7 |
8 | # Do any other automated setup that you need to do here
9 |
--------------------------------------------------------------------------------
/examples/includable_childs.rabl:
--------------------------------------------------------------------------------
1 | this = locals[:self_resource]
2 |
3 | unless inclusion_field(this).blank?
4 | inclusion_field(this).each_pair do |field_name, includable_field|
5 | # use this if fieldset is used
6 | # next if fieldset(this).present? && !fieldset(this).include?(field_name)
7 |
8 | # include that child
9 | if inclusion[this].include?(field_name)
10 |
11 | child field_name.to_sym, root: field_name.to_sym, object_root: false do
12 | template = (includable_field[:resource_name] || field_name).to_s.underscore.singularize
13 | extends template
14 | end
15 |
16 | # not to include that child
17 | else
18 |
19 | node field_name do |obj|
20 | if obj.try(includable_field[:id_field]).present?
21 | obj.try(includable_field[:id_field])
22 | else
23 | foreign_key = obj.class.try("#{field_name}_foreign_key")
24 | obj.try(foreign_key) if foreign_key.present?
25 | end
26 | end
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/gemfiles/grape_0.10.0.gemfile:
--------------------------------------------------------------------------------
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 => "../"
9 |
--------------------------------------------------------------------------------
/gemfiles/grape_0.11.0.gemfile:
--------------------------------------------------------------------------------
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 => "../"
9 |
--------------------------------------------------------------------------------
/gemfiles/rails_4.0.0.gemfile:
--------------------------------------------------------------------------------
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 => "../"
9 |
--------------------------------------------------------------------------------
/gemfiles/rails_4.1.0.gemfile:
--------------------------------------------------------------------------------
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 => "../"
9 |
--------------------------------------------------------------------------------
/gemfiles/rails_4.1.8.gemfile:
--------------------------------------------------------------------------------
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 => "../"
9 |
--------------------------------------------------------------------------------
/gemfiles/rails_4.2.0.gemfile:
--------------------------------------------------------------------------------
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 => "../"
9 |
--------------------------------------------------------------------------------
/gemfiles/rails_5.0.0.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "rails", "5.0.0.beta2"
6 | gem "rspec-rails"
7 |
8 | gemspec :path => "../"
9 |
--------------------------------------------------------------------------------
/lib/api_helper.rb:
--------------------------------------------------------------------------------
1 | require "api_helper/version"
2 | require "api_helper/fieldsettable"
3 | require "api_helper/includable"
4 | require "api_helper/paginatable"
5 | require "api_helper/sortable"
6 | require "api_helper/filterable"
7 | require "api_helper/multigettable"
8 |
9 | module APIHelper
10 | end
11 |
--------------------------------------------------------------------------------
/lib/api_helper/fieldsettable.rb:
--------------------------------------------------------------------------------
1 | require 'active_support'
2 | require 'active_support/core_ext/object/blank'
3 |
4 | # = Fieldsettable
5 | #
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.
10 | #
11 | # This design made references to the rules of Sparse Fieldsets in
12 | # JSON API:
13 | # http://jsonapi.org/format/#fetching-sparse-fieldsets
14 | #
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.
18 | #
19 | # GET /users?fields=id,name,avatar_url
20 | #
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 fields[object_type] parameters.
24 | #
25 | # GET /posts?fields[posts]=id,title,author&fields[user]=id,name,avatar_url
26 | #
27 | # Note: +author+ of a +post+ is a +user+.
28 | #
29 | # The +fields+ and fields[object_type] parameters can not be mixed.
30 | # If the latter format is used, then it must be used for the main resource as
31 | # well.
32 | #
33 | # == Usage
34 | #
35 | # Include this +Concern+ in your Action Controller:
36 | #
37 | # SamplesController < ApplicationController
38 | # include APIHelper::Fieldsettable
39 | # end
40 | #
41 | # or in your Grape API class:
42 | #
43 | # class SampleAPI < Grape::API
44 | # helpers APIHelper::Fieldsettable
45 | # end
46 | #
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:
57 | #
58 | # resources :posts do
59 | # get do
60 | # fieldset_for :post, default: true, default_fields: [:id, :title, :author]
61 | # fieldset_for :user, permitted_fields: [:id, :name, :posts, :avatar_url],
62 | # defaults_to_permitted_fields: true
63 | # # ...
64 | # end
65 | # end
66 | #
67 | # The +fieldset_for+ method used above parses the +fields+ and/or
68 | # fields[resource_name] parameters, and save the results into
69 | # +@fieldset+ instance variable for further usage.
70 | #
71 | # After that line, you can use the +fieldset+ helper method to get the fieldset
72 | # information. Actual examples are:
73 | #
74 | # With GET /posts?fields=title,author:
75 | #
76 | # fieldset #=> { post: [:title, :author], user: [:id, :name, :posts, :avatar_url] }
77 | #
78 | # With GET /posts?fields[post]=title,author&fields[user]=name:
79 | #
80 | # fieldset #=> { post: [:title, :author], user: [:name] }
81 | # fieldset(:post) #=> [:title, :author]
82 | # fieldset(:post, :title) #=> true
83 | # fieldset(:user, :avatar_url) #=> false
84 | #
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
100 | #
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])
111 | #
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:
116 | #
117 | # object @user
118 | #
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
122 | # set_fieldset :user, default_fields: [:id, :name, :avatar_url],
123 | # permitted_fields: [:id, :name, :avatar_url, :posts]
124 | #
125 | # # determine the fields to show on the fly
126 | # attributes(*fieldset[:user])
127 | #
128 | module APIHelper::Fieldsettable
129 | extend ActiveSupport::Concern
130 |
131 | # Gets the fields parameters, organize them into a +@fieldset+ hash for model to select certain.
132 | # fields and/or templates to render specified fieldset. Following the URL rules of JSON API:
133 | # http://jsonapi.org/format/#fetching-sparse-fieldsets
134 | #
135 | # Params:
136 | #
137 | # +resource+::
138 | # +Symbol+ name of resource to receive the fieldset
139 | #
140 | # +default+::
141 | # +Boolean+ should this resource take the parameter from +fields+ while no
142 | # resourse name is specified?
143 | #
144 | # +permitted_fields+::
145 | # +Array+ of +Symbol+s list of accessible fields used to filter out unpermitted fields,
146 | # defaults to permit all
147 | #
148 | # +default_fields+::
149 | # +Array+ of +Symbol+s list of fields to show by default
150 | #
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
154 | #
155 | # Example Result:
156 | #
157 | # fieldset_for :user, root: true
158 | # fieldset_for :group
159 | #
160 | # # @fieldset => {
161 | # # :user => [:id, :name, :email, :groups],
162 | # # :group => [:id, :name]
163 | # # }
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
170 |
171 | # put the fields in place
172 | if params[:fields].is_a?(Hash) ||
173 | defined?(Rails) && Rails.version.to_i >= 5 && params[:fields].is_a?(ActionController::Parameters)
174 | # get the specific resource fields from fields hash
175 | @fieldset[resource] = params[:fields][resource] || params[:fields][resource]
176 | elsif default
177 | # or get the fields string directly if this resource is th default one
178 | @fieldset[resource] = params[:fields]
179 | end
180 |
181 | # splits the string into array
182 | if @fieldset[resource].present?
183 | @fieldset[resource] = @fieldset[resource].split(',').map(&:to_s)
184 | else
185 | @fieldset[resource] = default_fields.map(&:to_s)
186 | end
187 |
188 | if permitted_fields.present?
189 | permitted_fields = permitted_fields.map(&:to_s)
190 |
191 | # filter out unpermitted fields by intersecting them
192 | @fieldset[resource] &= permitted_fields if @fieldset[resource].present?
193 |
194 | # set default fields to permitted_fields if needed
195 | @fieldset[resource] = permitted_fields if @fieldset[resource].blank? &&
196 | defaults_to_permitted_fields
197 | end
198 | end
199 |
200 | # Getter for the fieldset data
201 | #
202 | # This method will act as a traditional getter of the fieldset data and
203 | # returns a hash containing fields for each resource if no parameter is
204 | # provided.
205 | #
206 | # fieldset # => { 'user' => ['name'], 'post' => ['title', 'author'] }
207 | #
208 | # If one parameter - a specific resourse name is passed in, it will return
209 | # a fields array of that specific resourse.
210 | #
211 | # fieldset(:post) # => ['title', 'author']
212 | #
213 | # And if one more parameter - a field name, is passed in, it will return a
214 | # boolen, determining if that field should exist in that resource.
215 | #
216 | # fieldset(:post, :title) # => true
217 | #
218 | def fieldset(resource = nil, field = nil)
219 | # act as a traditional getter if no parameters specified
220 | if resource.blank?
221 | @fieldset ||= ActiveSupport::HashWithIndifferentAccess.new
222 |
223 | # returns the fieldset array if an specific resource is passed in
224 | elsif field.blank?
225 | fieldset[resource] || []
226 |
227 | # determine if a field is inculded in a specific fieldset
228 | else
229 | field = field.to_s
230 | fieldset(resource).is_a?(Array) && fieldset(resource).include?(field)
231 | end
232 | end
233 |
234 | # View Helper to set the default and permitted fields
235 | #
236 | # This is useful while using an resource view shared by multiple controllers,
237 | # it will ensure the +@fieldset+ instance variable presents, and can also set
238 | # the default fields of a model for convenience, or the whitelisted permitted
239 | # fields for security.
240 | def set_fieldset(resource, default_fields: [], permitted_fields: [])
241 | @fieldset ||= ActiveSupport::HashWithIndifferentAccess.new
242 | @fieldset[resource] = default_fields.map(&:to_s) if @fieldset[resource].blank?
243 | @fieldset[resource] &= permitted_fields.map(&:to_s) if permitted_fields.present?
244 | end
245 |
246 | # Returns the description of the 'fields' URL parameter
247 | def self.fields_param_desc(example: nil)
248 | if example.present?
249 | "Choose the fields to be returned. Example value: '#{example}'"
250 | else
251 | "Choose the fields to be returned."
252 | end
253 | end
254 |
255 | included do
256 | if defined? helper_method
257 | helper_method :fieldset, :set_fieldset
258 | end
259 | end
260 | end
261 |
--------------------------------------------------------------------------------
/lib/api_helper/filterable.rb:
--------------------------------------------------------------------------------
1 | require 'active_support'
2 | require 'active_support/core_ext/object/blank'
3 |
4 | # = Filterable
5 | #
6 | # A filterable resource API supports requests to filter resources in collection
7 | # by their fields, using the +filter+ query parameter.
8 | #
9 | # For example, the following is a request for all products that has a
10 | # particular color:
11 | #
12 | # GET /products?filter[color]=red
13 | #
14 | # With this approach, multiple filters can be applied to a single request:
15 | #
16 | # GET /products?filter[color]=red&filter[status]=in-stock
17 | #
18 | # Multiple filters are applied with the AND condition.
19 | #
20 | # A list separated by commas (",") can be used to filter by field matching one
21 | # of the values:
22 | #
23 | # GET /products?filter[color]=red,blue,yellow
24 | #
25 | # A few functions: +not+, +greater_then+, +less_then+, +greater_then_or_equal+,
26 | # +less_then_or_equal+, +between+, +like+, +contains+, +null+ and +blank+ can
27 | # be used to filter the data, for example:
28 | #
29 | # GET /products?filter[color]=not(red)
30 | # GET /products?filter[price]=greater_then(1000)
31 | # GET /products?filter[price]=less_then_or_equal(2000)
32 | # GET /products?filter[price]=between(1000,2000)
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()
37 | #
38 | # == Usage
39 | #
40 | # Include this +Concern+ in your Action Controller:
41 | #
42 | # SamplesController < ApplicationController
43 | # include APIHelper::Filterable
44 | # end
45 | #
46 | # or in your Grape API class:
47 | #
48 | # class SampleAPI < Grape::API
49 | # helpers APIHelper::Filterable
50 | # end
51 | #
52 | # then use the +filter+ method in the controller like this:
53 | #
54 | # @products = filter(Post, filterable_fields: [:name, :price, :color])
55 | #
56 | # The +filter+ method will return a scoped model collection, based
57 | # directly from the requested URL parameters.
58 | #
59 | module APIHelper::Filterable
60 | extend ActiveSupport::Concern
61 |
62 | # Filter resources of a collection from the request parameter
63 | #
64 | # Params:
65 | #
66 | # +resource+::
67 | # +ActiveRecord::Relation+ resource collection
68 | # to filter data from
69 | #
70 | # +filterable_fields+::
71 | # +Array+ of +Symbol+s fields that are allowed to be filtered, defaults
72 | # to all
73 | #
74 | def filter(resource, filterable_fields: [], ignore_unknown_fields: true)
75 | # parse the request parameter
76 | if params[:filter].is_a?(Hash) ||
77 | defined?(Rails) && Rails.version.to_i >= 5 && params[:filter].is_a?(ActionController::Parameters)
78 | @filter = params[:filter]
79 | filterable_fields = filterable_fields.map(&:to_s)
80 |
81 | # deal with each condition
82 | @filter.each_pair do |field, condition|
83 | # bypass fields that aren't be abled to filter with
84 | next if filterable_fields.present? && !filterable_fields.include?(field)
85 |
86 | # escape string to prevent SQL injection
87 | field = resource.connection.quote_string(field)
88 |
89 | next if ignore_unknown_fields && resource.columns_hash[field].blank?
90 | field_type = resource.columns_hash[field] && resource.columns_hash[field].type || :unknown
91 |
92 | # if a function is used
93 | if func = condition.match(/(?[^\(\)]+)\((?.*)\)/)
94 |
95 | db_column_name = begin
96 | raise if ActiveRecord::Base.configurations[Rails.env]['adapter'] != 'mysql2'
97 | "`#{resource.table_name}`.`#{field}`"
98 | rescue
99 | "\"#{resource.table_name}\".\"#{field}\""
100 | end
101 |
102 | case func[:function]
103 | when 'not'
104 | values = func[:param].split(',')
105 | values.map!(&:to_bool) if field_type == :boolean
106 | resource = resource.where.not(field => values)
107 |
108 | when 'greater_then'
109 | resource = resource
110 | .where("#{db_column_name} > ?",
111 | func[:param])
112 |
113 | when 'less_then'
114 | resource = resource
115 | .where("#{db_column_name} < ?",
116 | func[:param])
117 |
118 | when 'greater_then_or_equal'
119 | resource = resource
120 | .where("#{db_column_name} >= ?",
121 | func[:param])
122 |
123 | when 'less_then_or_equal'
124 | resource = resource
125 | .where("#{db_column_name} <= ?",
126 | func[:param])
127 |
128 | when 'between'
129 | param = func[:param].split(',')
130 | resource = resource
131 | .where("#{db_column_name} BETWEEN ? AND ?",
132 | param.first, param.last)
133 |
134 | when 'like'
135 | resource = resource
136 | .where("#{db_column_name} LIKE ?",
137 | func[:param])
138 |
139 | when 'contains'
140 | resource = resource
141 | .where("#{db_column_name} LIKE ?",
142 | "%#{func[:param]}%")
143 |
144 | when 'null'
145 | resource = resource.where(field => nil)
146 |
147 | when 'blank'
148 | resource = resource.where(field => [nil, ''])
149 | end
150 |
151 | # if not function
152 | else
153 | values = condition.split(',')
154 | values.map!(&:to_bool) if field_type == :boolean
155 | resource = resource.where(field => values)
156 | end
157 | end
158 | end
159 |
160 | return resource
161 | end
162 |
163 | # Returns a description of the 'fields' URL parameter
164 | def self.filter_param_desc(for_field: nil)
165 | if for_field.present?
166 | "Filter data base on the '#{for_field}' field."
167 | else
168 | "Filter the data."
169 | end
170 | end
171 | end
172 |
173 | class String
174 | def to_bool
175 | self == 'true'
176 | end
177 | end
178 |
--------------------------------------------------------------------------------
/lib/api_helper/includable.rb:
--------------------------------------------------------------------------------
1 | require 'active_support'
2 |
3 | # = Includable
4 | #
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 | #
9 | # This design made references to the rules of Inclusion of Related
10 | # Resources in JSON API:
11 | # http://jsonapi.org/format/#fetching-includes
12 | #
13 | # For instance, articles can be requested with their comments along:
14 | #
15 | # GET /articles?include=comments
16 | #
17 | # The server will respond
18 | #
19 | # [
20 | # {
21 | # "id": 1,
22 | # "title": "First Post",
23 | # "content": "...",
24 | # "comments": [
25 | # {
26 | # "id": 1,
27 | # "content": "..."
28 | # },
29 | # {
30 | # "id": 3,
31 | # "content": "..."
32 | # },
33 | # {
34 | # "id": 6,
35 | # "content": "..."
36 | # }
37 | # ]
38 | # },
39 | # {
40 | # "id": 2,
41 | # "title": "Second Post",
42 | # "content": "...",
43 | # "comments": [
44 | # {
45 | # "id": 2,
46 | # "content": "..."
47 | # },
48 | # {
49 | # "id": 4,
50 | # "content": "..."
51 | # },
52 | # {
53 | # "id": 5,
54 | # "content": "..."
55 | # }
56 | # ]
57 | # }
58 | # ]
59 | #
60 | # instead of just the ids of each comment
61 | #
62 | # [
63 | # {
64 | # "id": 1,
65 | # "title": "First Post",
66 | # "content": "...",
67 | # "comments": [1, 3, 6]
68 | # },
69 | # {
70 | # "id": 2,
71 | # "title": "Second Post",
72 | # "content": "...",
73 | # "comments": [2, 4, 5]
74 | # }
75 | # ]
76 | #
77 | # Multiple related resources can be stated in a comma-separated list,
78 | # like this:
79 | #
80 | # GET /articles/12?include=author,comments
81 | #
82 | # == Usage
83 | #
84 | # Include this +Concern+ in your Action Controller:
85 | #
86 | # SamplesController < ApplicationController
87 | # include APIHelper::Includable
88 | # end
89 | #
90 | # or in your Grape API class:
91 | #
92 | # class SampleAPI < Grape::API
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 | # # ...
101 | # end
102 | #
103 | # or in the Grape method if you're using it:
104 | #
105 | # resources :posts do
106 | # get do
107 | # inclusion_for :post, default: true
108 | # # ...
109 | # end
110 | # end
111 | #
112 | # This helper parses the +include+ and/or include[resource_name]
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!
125 | #
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:
129 | #
130 | # @posts = Post.includes(inclusion(:post))
131 | #
132 | # The +inclusion+ helper method will return data depending on the parameters
133 | # passed in, as the following example:
134 | #
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!
141 | #
142 | # === API View with RABL
143 | #
144 | # If you're using RABL as the API view, it can be setup like this:
145 | #
146 | # object @post
147 | #
148 | # # set the includable and default inclusion fields of the view
149 | # set_inclusion :post, default_includes: [:author]
150 | #
151 | # # set the details for all includable fields
152 | # set_inclusion_field :post, :author, :author_id
153 | # set_inclusion_field :post, :comments, :comment_ids
154 | #
155 | # # extends the partial to show included fields
156 | # extends('extensions/includable_childs', locals: { self_resource: :post })
157 | #
158 | # --
159 | # TODO: provide an example of includable_childs.rabl
160 | # ++
161 | #
162 | module APIHelper::Includable
163 | extend ActiveSupport::Concern
164 |
165 | # Gets the include parameters, organize them into a +@inclusion+ hash
166 | #
167 | # Params:
168 | #
169 | # +resource+::
170 | # +Symbol+ name of resource to receive the inclusion
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 | defined?(Rails) && Rails.version.to_i >= 5 && params[:include].is_a?(ActionController::Parameters)
197 | # get the specific resource inclusion fields from the "include" hash
198 | @inclusion[resource] = params[:include][resource]
199 | @inclusion_specified[resource] = true if params[:include][resource].present?
200 | elsif default
201 | # or get the "include" string directly if this resource is th default one
202 | @inclusion[resource] = params[:include]
203 | @inclusion_specified[resource] = true if params[:include].present?
204 | end
205 |
206 | # splits the string into array
207 | if @inclusion[resource].present?
208 | @inclusion[resource] = @inclusion[resource].split(',').map(&:to_s)
209 | elsif !@inclusion_specified[resource]
210 | @inclusion[resource] = default_includes.map(&:to_s)
211 | end
212 |
213 | if permitted_includes.present?
214 | permitted_includes = permitted_includes.map(&:to_s)
215 |
216 | # filter out unpermitted includes by intersecting them
217 | @inclusion[resource] &= permitted_includes if @inclusion[resource].present?
218 |
219 | # set default inclusion to permitted_includes if needed
220 | @inclusion[resource] = permitted_includes if @inclusion[resource].blank? &&
221 | defaults_to_permitted_includes &&
222 | !@inclusion_specified[resource]
223 | end
224 |
225 | if @fieldset.is_a?(Hash) && @fieldset[resource].present?
226 | @inclusion[resource] &= @fieldset[resource]
227 | end
228 | end
229 |
230 | # Getter for the inclusion data
231 | #
232 | # This method will act as a traditional getter of the inclusion data and
233 | # returns a hash containing fields for each resource if no parameter is
234 | # provided.
235 | #
236 | # inclusion # => { 'post' => ['author', 'comments'] }
237 | #
238 | # If one parameter - a specific resourse name is passed in, it will return an
239 | # array of relation names that should be included for that specific resourse.
240 | #
241 | # inclusion(:post) # => ['author', 'comments']
242 | #
243 | # And if one more parameter - a field name, is passed in, it will return a
244 | # boolen, determining if that relation should be included in the response.
245 | #
246 | # inclusion(:post, :author) # => true
247 | #
248 | def inclusion(resource = nil, field = nil)
249 | # act as a traditional getter if no parameters specified
250 | if resource.blank?
251 | @inclusion ||= ActiveSupport::HashWithIndifferentAccess.new
252 |
253 | # returns the inclusion array if an specific resource is passed in
254 | elsif field.blank?
255 | inclusion[resource] || []
256 |
257 | # determine if a field is inculded
258 | else
259 | field = field.to_s
260 | inclusion(resource).is_a?(Array) && inclusion(resource).include?(field)
261 | end
262 | end
263 |
264 | # View Helper to set the inclusion
265 | #
266 | # This is useful while using an resource view shared by multiple controllers,
267 | # this will ensure the +@inclusion+ instance variable presents, and can also
268 | # set the default included fields of a model for convenience, or the fields
269 | # that are permitted to be included for security.
270 | def set_inclusion(resource, default_includes: [], permitted_includes: [])
271 | @inclusion ||= ActiveSupport::HashWithIndifferentAccess.new
272 | @inclusion_field ||= ActiveSupport::HashWithIndifferentAccess.new
273 | @inclusion_specified ||= ActiveSupport::HashWithIndifferentAccess.new
274 | @inclusion[resource] = default_includes.map(&:to_s) if @inclusion[resource].blank? &&
275 | !@inclusion_specified[resource]
276 | @inclusion[resource] &= permitted_includes.map(&:to_s) if permitted_includes.present?
277 |
278 | if @fieldset.is_a?(Hash) && @fieldset[resource].present?
279 | @inclusion[resource] &= @fieldset[resource]
280 | end
281 | end
282 |
283 | # View Helper to set the inclusion details
284 | #
285 | # Params:
286 | #
287 | # +resource+::
288 | # +Symbol+ name of the resource to receive the inclusion field data
289 | #
290 | # +field+::
291 | # +Symbol+ the field name of the relatiion that can be included
292 | #
293 | # +id_field+::
294 | # +Symbol+ the field to use (normally suffixed with "_id") if the object
295 | # isn't included
296 | #
297 | # +resource_name+::
298 | # +Symbol+ the name of the child resource, can be used to determine which
299 | # view template should be extended for rendering that child node and also
300 | # can shown in the response metadata as well
301 | #
302 | # +resources_url+::
303 | # +String+ the resources URL of the child resource, can be used to be shown
304 | # in the metadata for the clients' convenience to learn ablou the API
305 | #
306 | def set_inclusion_field(resource, field, id_field, resource_name: nil,
307 | resources_url: nil)
308 | @inclusion_field ||= ActiveSupport::HashWithIndifferentAccess.new
309 | @inclusion_field[resource] ||= ActiveSupport::HashWithIndifferentAccess.new
310 | @inclusion_field[resource][field] = {
311 | field: field,
312 | id_field: id_field,
313 | resource_name: resource_name,
314 | resources_url: resources_url
315 | }
316 | end
317 |
318 | # Getter for the data of includable fields
319 | #
320 | # Params:
321 | #
322 | # +resource+::
323 | # +Symbol+ the resource name of inclusion data to retrieve
324 | #
325 | def inclusion_field(resource = nil)
326 | # act as a traditional getter if no parameters specified
327 | if resource.blank?
328 | @inclusion_field ||= ActiveSupport::HashWithIndifferentAccess.new
329 |
330 | # returns the inclusion array if an specific resource is passed in
331 | else
332 | inclusion_field[resource] || {}
333 | end
334 | end
335 |
336 | # Returns the description of the 'include' URL parameter
337 | def self.include_param_desc(example: nil, default: nil)
338 | if default.present?
339 | desc = "Returning compound documents that include specific associated objects, defaults to '#{default}'."
340 | else
341 | desc = "Returning compound documents that include specific associated objects."
342 | end
343 |
344 | if example.present?
345 | "#{desc} Example value: '#{example}'"
346 | else
347 | desc
348 | end
349 | end
350 |
351 | included do
352 | if defined? helper_method
353 | helper_method :inclusion, :set_inclusion, :set_inclusion_field, :inclusion_field
354 | end
355 | end
356 | end
357 |
--------------------------------------------------------------------------------
/lib/api_helper/multigettable.rb:
--------------------------------------------------------------------------------
1 | require 'active_support'
2 |
3 | # = Multigettable
4 | #
5 | # A normal RESTful API can let clients get one specified resource at a time:
6 | #
7 | # GET /posts/3
8 | #
9 | # If it's declared to be multigettable, then clients can get multiple
10 | # specified resource like this:
11 | #
12 | # GET /posts/3,4,8,9
13 | #
14 | # An API may also support applying operations on multiple resource sith a
15 | # single request using this approach
16 | #
17 | # PATCH /posts/3,4,8,9
18 | # DELETE /posts/3,4,8,9
19 | #
20 | # == Usage
21 | #
22 | # Include this +Concern+ in your Action Controller:
23 | #
24 | # SamplesController < ApplicationController
25 | # include APIHelper::Multigettable
26 | # end
27 | #
28 | # or in your Grape API class:
29 | #
30 | # class SampleAPI < Grape::API
31 | # helpers APIHelper::Multigettable
32 | # end
33 | #
34 | # then use the +multiget+ method like this:
35 | #
36 | # @post = multiget(Post, find_by: :id, param: :id, max: 12)
37 | #
38 | # The +multiget+ method returns an collection of or a single model, based
39 | # directly from the requested URL.
40 | #
41 | # There is also another helper method to determine whether the request is
42 | # multigeting or not:
43 | #
44 | # multiget?(param: id) # => true of false
45 | #
46 | module APIHelper::Multigettable
47 | extend ActiveSupport::Concern
48 |
49 | # Get multiple resources from the request by specifing ids split by ','
50 | #
51 | # Params:
52 | #
53 | # +resource+::
54 | # +ActiveRecord::Relation+ resource collection to find resources from
55 | #
56 | # +find_by+::
57 | # +Symbol+ the attribute that is used to find resources, defaults to :id
58 | #
59 | # +param+::
60 | # +Symbol+ the request parameter name used to find resources,
61 | # defaults to :id
62 | #
63 | # +max+::
64 | # +Integer+ maxium count of returning results
65 | #
66 | def multiget(resource, find_by: :id, param: :id, max: 10)
67 | ids = params[param].split(',')
68 | ids = ids[0..(max - 1)]
69 |
70 | if ids.count > 1
71 | resource.where(find_by => ids)
72 | else
73 | resource.find_by(find_by => ids[0])
74 | end
75 | end
76 |
77 | # Is the a multiget request?
78 | def multiget?(param: :id)
79 | params[param].present? && params[param].include?(',')
80 | end
81 | end
82 |
--------------------------------------------------------------------------------
/lib/api_helper/paginatable.rb:
--------------------------------------------------------------------------------
1 | require 'active_support'
2 |
3 | # = Paginatable
4 | #
5 | # Paginating the requested items can avoid returning too much data in a single
6 | # response. API clients can iterate over the results with pagination instead of
7 | # rerteving all the data in one time, ruining the database connection or
8 | # network.
9 | #
10 | # There are two available URL parameters: +per_page+ and +page+. The former is
11 | # used for setting how many resources will be returned in each page, there will
12 | # be a maxium limit and default value for each API:
13 | #
14 | # GET /posts?per_page=10
15 | #
16 | # The server will respond 10 resources in a request.
17 | #
18 | # Use the +page+ parameter to specify which to page get:
19 | #
20 | # GET /posts?page=5
21 | #
22 | # Pagination info will be provided in the HTTP Link header like this:
23 | #
24 | # Link: ; rel="first",
25 | # ; rel="prev"
26 | # ; rel="next",
27 | # ; rel="last"
28 | #
29 | # Line breaks are added for readability.
30 | #
31 | # Which follows the proposed RFC 5988 standard.
32 | #
33 | # An aditional header, +X-Items-Count+, will also be set to the total pages
34 | # count.
35 | #
36 | # == Usage
37 | #
38 | # Include this +Concern+ in your Action Controller:
39 | #
40 | # SamplesController < ApplicationController
41 | # include APIHelper::Paginatable
42 | # end
43 | #
44 | # or in your Grape API class:
45 | #
46 | # class SampleAPI < Grape::API
47 | # helpers APIHelper::Paginatable
48 | # end
49 | #
50 | # then set the options for pagination in the grape method, as the following as
51 | # an example:
52 | #
53 | # resources :posts do
54 | # get do
55 | # collection = current_user.posts
56 | # pagination collection.count, default_per_page: 25, maxium_per_page: 100
57 | #
58 | # # ...
59 | # end
60 | # end
61 | #
62 | # Then use the helper methods like this:
63 | #
64 | # # this example uses kaminari
65 | # User.page(pagination_per_page).per(pagination_page)
66 | #
67 | # HTTP Link header will be automatically set by the way.
68 | module APIHelper::Paginatable
69 | extend ActiveSupport::Concern
70 |
71 | # Set pagination for the request
72 | #
73 | # Params:
74 | #
75 | # +items_count+::
76 | # +Symbol+ name of resource to receive the inclusion
77 | #
78 | # +default_per_page+::
79 | # +Integer+ default per_page
80 | #
81 | # +maxium_per_page+::
82 | # +Integer+ maximum results do return on a single page
83 | #
84 | def pagination(items_count, default_per_page: 20,
85 | maxium_per_page: 100,
86 | set_header: true)
87 | items_count = items_count.count if items_count.respond_to? :count
88 |
89 | @pagination_per_page = (params[:per_page] || default_per_page).to_i
90 | @pagination_per_page = maxium_per_page if @pagination_per_page > maxium_per_page
91 | @pagination_per_page = 1 if @pagination_per_page < 1
92 |
93 | items_count = 0 if items_count < 0
94 | pages_count = (items_count.to_f / @pagination_per_page).ceil
95 | pages_count = 1 if pages_count < 1
96 |
97 | @pagination_items_count = items_count
98 | @pagination_pages_count = pages_count
99 |
100 | @pagination_page = (params[:page] || 1).to_i
101 | @pagination_page = pages_count if @pagination_page > pages_count
102 | @pagination_page = 1 if @pagination_page < 1
103 |
104 | if current_page > 1
105 | @pagination_first_page_url = add_or_replace_uri_param(request.url, :page, 1)
106 | @pagination_prev_page_url = add_or_replace_uri_param(request.url, :page, (current_page > pages_count ? pages_count : current_page - 1))
107 | end
108 |
109 | if current_page < pages_count
110 | @pagination_next_page_url = add_or_replace_uri_param(request.url, :page, current_page + 1)
111 | @pagination_last_page_url = add_or_replace_uri_param(request.url, :page, pages_count)
112 | end
113 |
114 | if set_header
115 | link_headers ||= []
116 |
117 | if current_page > 1
118 | link_headers << "<#{@pagination_first_page_url}>; rel=\"first\"" if @pagination_first_page_url
119 | link_headers << "<#{@pagination_prev_page_url}>; rel=\"prev\"" if @pagination_prev_page_url
120 | end
121 |
122 | if current_page < pages_count
123 | link_headers << "<#{@pagination_next_page_url}>; rel=\"next\"" if @pagination_next_page_url
124 | link_headers << "<#{@pagination_last_page_url}>; rel=\"last\"" if @pagination_last_page_url
125 | end
126 |
127 | link_header = link_headers.join(', ')
128 |
129 | if self.respond_to?(:header)
130 | self.header('Link', link_header)
131 | self.header('X-Items-Count', items_count.to_s)
132 | self.header('X-Pages-Count', pages_count.to_s)
133 | end
134 |
135 | if defined?(response) && response.respond_to?(:headers)
136 | response.headers['Link'] = link_header
137 | response.headers['X-Items-Count'] = items_count.to_s
138 | response.headers['X-Pages-Count'] = pages_count.to_s
139 | end
140 | end
141 | end
142 |
143 | # Getter for the current page
144 | def pagination_page
145 | @pagination_page
146 | end
147 |
148 | alias_method :current_page, :pagination_page
149 |
150 | # Getter for per_page
151 | def pagination_per_page
152 | @pagination_per_page
153 | end
154 |
155 | def pagination_items_count
156 | @pagination_items_count
157 | end
158 |
159 | def pagination_pages_count
160 | @pagination_pages_count
161 | end
162 |
163 | def pagination_first_page_url
164 | @pagination_first_page_url
165 | end
166 |
167 | def pagination_prev_page_url
168 | @pagination_prev_page_url
169 | end
170 |
171 | def pagination_next_page_url
172 | @pagination_next_page_url
173 | end
174 |
175 | def pagination_last_page_url
176 | @pagination_last_page_url
177 | end
178 |
179 | alias_method :paginate_with, :pagination_per_page
180 |
181 | def add_or_replace_uri_param(url, param_name, param_value) # :nodoc:
182 | uri = URI(url)
183 | params = URI.decode_www_form(uri.query || '')
184 | params.delete_if { |param| param[0].to_s == param_name.to_s }
185 | params << [param_name, param_value]
186 | uri.query = URI.encode_www_form(params)
187 | uri.to_s
188 | end
189 |
190 | # Return the 'per_page' param description
191 | def self.per_page_param_desc
192 | "Specify how many items you want each page to return."
193 | end
194 |
195 | # Return the 'page' param description
196 | def self.page_param_desc
197 | "Specify which page you want to get."
198 | end
199 | end
200 |
--------------------------------------------------------------------------------
/lib/api_helper/sortable.rb:
--------------------------------------------------------------------------------
1 | require 'active_support'
2 |
3 | # = Sortable
4 | #
5 | # A Sortable Resource API gives the flexibility to change how the returned data
6 | # is sorted to the client. Clients can use the +sort+ URL parameter to control
7 | # how the returned data is sorted, as this example:
8 | #
9 | # GET /posts?sort=-created_at,title
10 | #
11 | # This means to sort the data by its created time descended and then the title
12 | # ascended.
13 | #
14 | # == Usage
15 | #
16 | # Include this +Concern+ in your Action Controller:
17 | #
18 | # SamplesController < ApplicationController
19 | # include APIHelper::Sortable
20 | # end
21 | #
22 | # or in your Grape API class:
23 | #
24 | # class SampleAPI < Grape::API
25 | # helpers APIHelper::Sortable
26 | # end
27 | #
28 | # then use the +sortable+ method like this:
29 | #
30 | # resources :posts do
31 | # get do
32 | # sortable default_order: { created_at: :desc }
33 | # @posts = Post.order(sortable_sort)
34 | #
35 | # # ...
36 | # end
37 | # end
38 | #
39 | module APIHelper::Sortable
40 | extend ActiveSupport::Concern
41 |
42 | # Gets the +sort+ parameter with the format 'resourses?sort=-created_at,name',
43 | # verify and converts it into an safe Hash that can be passed into the .order
44 | # method.
45 | #
46 | # Params:
47 | #
48 | # +default_order+::
49 | # +Hash+ the default value to return if the sort parameter is not provided
50 | #
51 | def sortable(default_order: {})
52 | # get the parameter
53 | sort_by = params[:sort] || params[:sort_by]
54 |
55 | if sort_by.is_a?(String)
56 | # split it
57 | sort_by_attrs = sort_by.gsub(/[^a-zA-Z0-9\-_,]/, '').split(',')
58 |
59 | # save it
60 | @sortable_sort = {}
61 | sort_by_attrs.each do |attrb|
62 | if attrb.match(/^-/)
63 | @sortable_sort[attrb.gsub(/^-/, '')] = :desc
64 | else
65 | @sortable_sort[attrb] = :asc
66 | end
67 | end
68 | else
69 | @sortable_sort = default_order
70 | end
71 | end
72 |
73 | # Helper to get the sort data
74 | def sortable_sort
75 | @sortable_sort
76 | end
77 |
78 | # Return the 'sort' param description
79 | def self.sort_param_desc(example: nil, default: nil)
80 | if default.present?
81 | desc = "Specify how the returning data should be sorted, defaults to '#{default}'."
82 | else
83 | desc = "Specify how the returning data should be sorted."
84 | end
85 | if example.present?
86 | "#{desc} Example value: '#{example}'"
87 | else
88 | desc
89 | end
90 | end
91 | end
92 |
--------------------------------------------------------------------------------
/lib/api_helper/version.rb:
--------------------------------------------------------------------------------
1 | module APIHelper
2 | VERSION = "0.1.3"
3 | end
4 |
--------------------------------------------------------------------------------
/spec/api_helper/fieldsettable_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe APIHelper::Fieldsettable do
4 | describe ".fields_param_desc" do
5 | it "returns a string" do
6 | expect(APIHelper::Fieldsettable.fields_param_desc).to be_a(String)
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/spec/api_helper/filterable_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe APIHelper::Filterable do
4 | describe ".filter_param_desc" do
5 | it "returns a string" do
6 | expect(APIHelper::Filterable.filter_param_desc).to be_a(String)
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/spec/api_helper/grape/fieldsettable_spec.rb:
--------------------------------------------------------------------------------
1 | require 'grape_helper'
2 |
3 | describe APIHelper::Fieldsettable do
4 | context "used in a Grape app" do
5 | include Rack::Test::Methods
6 |
7 | class FieldsettableAPI < Grape::API
8 | helpers APIHelper::Fieldsettable
9 |
10 | resources :posts do
11 | get do
12 | fieldset_for :post, default: true,
13 | permitted_fields: [:title, :content, :author],
14 | default_fields: [:title, :content]
15 | fieldset_for :user, permitted_fields: [:id, :avatar_url, :name],
16 | defaults_to_permitted_fields: true
17 |
18 | # returns the fieldset directly for examining
19 | return fieldset
20 | end
21 | end
22 | end
23 |
24 | def app
25 | FieldsettableAPI
26 | end
27 |
28 | it "parses the currect fieldsets for each resource" do
29 | get '/posts.json?fields[post]=title,author&fields[user]=name'
30 | json = JSON.parse(last_response.body)
31 |
32 | expect(json).to eq('post' => ['title', 'author'], 'user' => ['name'])
33 | end
34 |
35 | it "uses the default fieldsets if not specified" do
36 | get '/posts.json'
37 | json = JSON.parse(last_response.body)
38 |
39 | expect(json).to eq('post' => ['title', 'content'], 'user' => ['id', 'avatar_url', 'name'])
40 | end
41 |
42 | it "ignores undeclared resource" do
43 | get '/posts.json?fields[post]=title,author&fields[ufo]=radius'
44 | json = JSON.parse(last_response.body)
45 |
46 | expect(json['post']).to eq(['title', 'author'])
47 | expect(json['ufo']).to be_blank
48 | end
49 |
50 | it "does not return unpermitted fields for an resource" do
51 | get '/posts.json?fields[post]=title,author,secret&fields[user]=name,password'
52 | json = JSON.parse(last_response.body)
53 |
54 | expect(json).to eq('post' => ['title', 'author'],
55 | 'user' => ['name'])
56 | expect(json['user']).not_to include('password')
57 | end
58 | end
59 | end if defined?(Grape)
60 |
--------------------------------------------------------------------------------
/spec/api_helper/grape/filterable_spec.rb:
--------------------------------------------------------------------------------
1 | require 'grape_helper'
2 |
3 | describe APIHelper::Filterable do
4 | context "used in a Grape app" do
5 | include Rack::Test::Methods
6 |
7 | class FilterableAPI < Grape::API
8 | helpers APIHelper::Filterable
9 |
10 | resources :resources do
11 | get do
12 | collection = filter(Model.all)
13 | return collection
14 | end
15 | end
16 | end
17 |
18 | def app
19 | FilterableAPI
20 | end
21 |
22 | it "filters out resource with a matching attribute" do
23 | get '/resources.json?filter[string]=yo'
24 | json = JSON.parse(last_response.body)
25 |
26 | expect(json).not_to be_blank
27 | json.each do |resource|
28 | expect(resource['string']).to eq('yo')
29 | end
30 |
31 | get '/resources.json?filter[boolean]=true'
32 | json = JSON.parse(last_response.body)
33 |
34 | expect(json).not_to be_blank
35 | json.each do |resource|
36 | expect(resource['boolean']).to be true
37 | end
38 | end
39 |
40 | it "filters out resource with multiple matching attribute" do
41 | get '/resources.json?filter[integer]=1,2,5,7'
42 | json = JSON.parse(last_response.body)
43 |
44 | expect(json).not_to be_blank
45 | json.each do |resource|
46 | expect([1, 2, 5, 7]).to include(resource['integer'])
47 | end
48 | end
49 |
50 | it "filters out resource with the \"not()\" function" do
51 | get '/resources.json?filter[string]=not(yo,hi,boom)'
52 | json = JSON.parse(last_response.body)
53 |
54 | expect(json).not_to be_blank
55 | json.each do |resource|
56 | expect(resource['string']).not_to eq('yo')
57 | expect(resource['string']).not_to eq('hi')
58 | expect(resource['string']).not_to eq('boom')
59 | end
60 | end
61 |
62 | it "filters out resource with the \"greater_then()\" function" do
63 | get '/resources.json?filter[integer]=greater_then(3)'
64 | json = JSON.parse(last_response.body)
65 |
66 | expect(json).not_to be_blank
67 | json.each do |resource|
68 | expect(resource['integer']).to be > 3
69 | end
70 | end
71 |
72 | it "filters out resource with the \"less_then()\" function" do
73 | get '/resources.json?filter[integer]=less_then(3)'
74 | json = JSON.parse(last_response.body)
75 |
76 | expect(json).not_to be_blank
77 | json.each do |resource|
78 | expect(resource['integer']).to be < 3
79 | end
80 | end
81 |
82 | it "filters out resource with the \"greater_then_or_equal()\" function" do
83 | get '/resources.json?filter[integer]=greater_then_or_equal(3)'
84 | json = JSON.parse(last_response.body)
85 |
86 | expect(json).not_to be_blank
87 | json.each do |resource|
88 | expect(resource['integer']).to be >= 3
89 | end
90 | end
91 |
92 | it "filters out resource with the \"less_then_or_equal()\" function" do
93 | get '/resources.json?filter[integer]=less_then_or_equal(3)'
94 | json = JSON.parse(last_response.body)
95 |
96 | expect(json).not_to be_blank
97 | json.each do |resource|
98 | expect(resource['integer']).to be <= 3
99 | end
100 | end
101 |
102 | it "filters out resource with the \"between()\" function" do
103 | get '/resources.json?filter[integer]=between(2,4)'
104 | json = JSON.parse(last_response.body)
105 |
106 | expect(json).not_to be_blank
107 | json.each do |resource|
108 | expect(resource['integer']).to be >= 2
109 | expect(resource['integer']).to be <= 4
110 | end
111 |
112 | get '/resources.json?filter[datetime]=between(1970-1-1,2199-1-1)'
113 | json = JSON.parse(last_response.body)
114 |
115 | expect(json).not_to be_blank
116 | json.each do |resource|
117 | expect(Time.new(resource['datetime'])).to be >= Time.new(1970, 1, 1)
118 | expect(Time.new(resource['datetime'])).to be <= Time.new(2199, 1, 1)
119 | end
120 | end
121 |
122 | it "filters out resource with the \"contains()\" function" do
123 | get '/resources.json?filter[string]=contains(o)'
124 | json = JSON.parse(last_response.body)
125 |
126 | expect(json).not_to be_blank
127 | json.each do |resource|
128 | expect(resource['string']).to include('o')
129 | end
130 | end
131 |
132 | it "filters out resource with the \"null()\" function" do
133 | get '/resources.json?filter[string]=null()'
134 | json = JSON.parse(last_response.body)
135 |
136 | expect(json).not_to be_blank
137 | json.each do |resource|
138 | expect(resource['string']).to eq(nil)
139 | end
140 |
141 | get '/resources.json?filter[boolean]=null()'
142 | json = JSON.parse(last_response.body)
143 |
144 | expect(json).not_to be_blank
145 | json.each do |resource|
146 | expect(resource['boolean']).to eq(nil)
147 | end
148 | end
149 |
150 | it "filters out resource with the \"blank()\" function" do
151 | get '/resources.json?filter[string]=blank()'
152 | json = JSON.parse(last_response.body)
153 |
154 | expect(json).not_to be_blank
155 | json.each do |resource|
156 | expect(resource['string']).to be_blank
157 | end
158 | end
159 |
160 | it "filters out resource with multiple conditions" do
161 | get '/resources.json?filter[integer]=between(2,4)&filter[string]=contains(o)'
162 | json = JSON.parse(last_response.body)
163 |
164 | expect(json).not_to be_blank
165 | json.each do |resource|
166 | expect(resource['integer']).to be_between(2, 4)
167 | expect(resource['string']).to include('o')
168 | end
169 | end
170 |
171 | it "ignores filtering with unknown fields" do
172 | get '/resources.json?filter[string]=%D1%85%D0%BE%D1%80%D0%BE%D1%88%D0%BE&unknown_field=val'
173 | json = JSON.parse(last_response.body)
174 |
175 | expect(json).not_to be_blank
176 | json.each do |resource|
177 | expect(resource['string']).to eq('хорошо')
178 | end
179 | end
180 |
181 | context "with filterable fields specified" do
182 |
183 | class FilterableAPIv2 < Grape::API
184 | helpers APIHelper::Filterable
185 |
186 | resources :resources do
187 | get do
188 | collection =
189 | filter(Model.all, filterable_fields: [:integer, :boolean])
190 | return collection
191 | end
192 | end
193 | end
194 |
195 | def app
196 | FilterableAPIv2
197 | end
198 |
199 | it "is only filterable with filterable fields" do
200 | get '/resources.json?filter[string]=%D1%85%D0%BE%D1%80%D0%BE%D1%88%D0%BE&integer=5'
201 | json = JSON.parse(last_response.body)
202 |
203 | expect(json).not_to be_blank
204 | expect(json.count).to be > 1
205 | end
206 | end
207 | end
208 | end if defined?(Grape)
209 |
--------------------------------------------------------------------------------
/spec/api_helper/grape/includable_spec.rb:
--------------------------------------------------------------------------------
1 | require 'grape_helper'
2 |
3 | describe APIHelper::Includable do
4 | context "used in a Grape app" do
5 | include Rack::Test::Methods
6 |
7 | class IncludableAPI < Grape::API
8 | helpers APIHelper::Includable
9 |
10 | resources :posts do
11 | get do
12 | inclusion_for :post, default: true, default_includes: [:author, :comments]
13 | inclusion_for :user, permitted_includes: [:posts, :followers]
14 |
15 | # returns the inclusion directly for examining
16 | return inclusion
17 | end
18 | end
19 | end
20 |
21 | def app
22 | IncludableAPI
23 | end
24 |
25 | it "parses the included fields for each resource" do
26 | get 'posts.json?include[post]=author,comments,stargazers&include[user]=followers'
27 | json = JSON.parse(last_response.body)
28 |
29 | expect(json).to eq('post' => ['author', 'comments', 'stargazers'],
30 | 'user' => ['followers'])
31 | end
32 |
33 | it "parses the currect fieldsets for the default resource" do
34 | get 'posts.json?include=author,comments,stargazers'
35 | json = JSON.parse(last_response.body)
36 |
37 | expect(json['post']).to eq(['author', 'comments', 'stargazers'])
38 | end
39 |
40 | it "uses the default fieldsets if not specified" do
41 | get 'posts.json'
42 | json = JSON.parse(last_response.body)
43 |
44 | expect(json['post']).to eq(['author', 'comments'])
45 | end
46 |
47 | it "ignores undeclared resource" do
48 | get 'posts.json?include[post]=author&include[ufo]=radius'
49 | json = JSON.parse(last_response.body)
50 |
51 | expect(json['post']).to eq(['author'])
52 | expect(json['ufo']).to be_blank
53 | end
54 |
55 | context "permitted includes is set" do
56 | it "ignores unpermitted fields" do
57 | get 'posts.json?include[post]=author&include[user]=followers,devices'
58 | json = JSON.parse(last_response.body)
59 |
60 | expect(json['post']).to eq(['author'])
61 | expect(json['user']).to eq(['followers'])
62 | end
63 |
64 | it "can be disabled while giving invalid parameters" do
65 | get 'posts.json?include[user]=none'
66 | json = JSON.parse(last_response.body)
67 |
68 | expect(json['user']).to be_blank
69 | end
70 | end
71 |
72 | context "integrated with fieldsettable" do
73 |
74 | class IncludableAPIv2 < Grape::API
75 | helpers APIHelper::Fieldsettable
76 | helpers APIHelper::Includable
77 |
78 | resources :posts do
79 | get do
80 | fieldset_for :post, default: true, default_fields: [:title, :author, :content]
81 | fieldset_for :user
82 | inclusion_for :post, default: true, default_includes: [:author, :comments]
83 | inclusion_for :user, permitted_includes: [:posts, :followers]
84 |
85 | # returns the inclusion directly for examining
86 | return inclusion
87 | end
88 | end
89 | end
90 |
91 | def app
92 | IncludableAPIv2
93 | end
94 |
95 | it "ignores fields not listed in fieldset" do
96 | get 'posts.json?include=author,comments'
97 | json = JSON.parse(last_response.body)
98 |
99 | expect(json['post']).to eq(['author'])
100 |
101 | get 'posts.json?fields=title,author,comments&include=author,comments'
102 | json = JSON.parse(last_response.body)
103 |
104 | expect(json['post']).to eq(['author', 'comments'])
105 | end
106 |
107 | it "includes fields by default" do
108 | get 'posts.json?fields=title,author,comments'
109 | json = JSON.parse(last_response.body)
110 |
111 | expect(json['post']).to eq(['author', 'comments'])
112 | end
113 | end
114 | end
115 | end if defined?(Grape)
116 |
--------------------------------------------------------------------------------
/spec/api_helper/grape/multigettable_spec.rb:
--------------------------------------------------------------------------------
1 | require 'grape_helper'
2 |
3 | describe APIHelper::Multigettable do
4 | context "used in a Grape app" do
5 | include Rack::Test::Methods
6 |
7 | class MultigettableAPI < Grape::API
8 | helpers APIHelper::Multigettable
9 |
10 | resources :resources do
11 | get :':id' do
12 | collection = multiget(Model.all)
13 | return collection
14 | end
15 | end
16 | end
17 |
18 | def app
19 | MultigettableAPI
20 | end
21 |
22 | it "can get a specified resource" do
23 | get '/resources/1.json'
24 | json = JSON.parse(last_response.body)
25 |
26 | expect(json['id']).to eq(1)
27 |
28 | get '/resources/2.json'
29 | json = JSON.parse(last_response.body)
30 |
31 | expect(json['id']).to eq(2)
32 | end
33 |
34 | it "can get multiple specified resource" do
35 | get '/resources/1,4,2,5.json'
36 | json = JSON.parse(last_response.body)
37 |
38 | expect(json.count).to eq(4)
39 | json.each do |resource|
40 | expect([1, 4, 2, 5]).to include(resource['id'])
41 | end
42 | end
43 | end
44 | end if defined?(Grape)
45 |
--------------------------------------------------------------------------------
/spec/api_helper/grape/paginatable_spec.rb:
--------------------------------------------------------------------------------
1 | require 'grape_helper'
2 |
3 | describe APIHelper::Paginatable do
4 | context "used in a Grape app" do
5 | include Rack::Test::Methods
6 |
7 | class PaginatableAPI < Grape::API
8 | helpers APIHelper::Paginatable
9 |
10 | resources :resources do
11 | get do
12 | pagination 1201, default_per_page: 20, maxium_per_page: 100
13 | return []
14 | end
15 | end
16 | end
17 |
18 | def app
19 | PaginatableAPI
20 | end
21 |
22 | it "sets the correct HTTP Link response header" do
23 | get 'resources.json'
24 | expect(last_response.header['Link']).to eq('; rel="next", ; rel="last"')
25 |
26 | get 'resources.json?page=2'
27 | expect(last_response.header['Link']).to eq('; rel="first", ; rel="prev", ; rel="next", ; rel="last"')
28 |
29 | get 'resources.json?page=3'
30 | expect(last_response.header['Link']).to eq('; rel="first", ; rel="prev", ; rel="next", ; rel="last"')
31 |
32 | get 'resources.json?page=60'
33 | expect(last_response.header['Link']).to eq('; rel="first", ; rel="prev", ; rel="next", ; rel="last"')
34 |
35 | get 'resources.json?page=61'
36 | expect(last_response.header['Link']).to eq('; rel="first", ; rel="prev"')
37 |
38 | get 'resources.json?page=62'
39 | expect(last_response.header['Link']).to eq('; rel="first", ; rel="prev"')
40 |
41 | get 'resources.json?page=0'
42 | expect(last_response.header['Link']).to eq('; rel="next", ; rel="last"')
43 |
44 | get 'resources.json?per_page=5&page=3'
45 | expect(last_response.header['Link']).to eq('; rel="first", ; rel="prev", ; rel="next", ; rel="last"')
46 |
47 | get 'resources.json?per_page=5000&page=3'
48 | expect(last_response.header['Link']).to eq('; rel="first", ; rel="prev", ; rel="next", ; rel="last"')
49 |
50 | get 'resources.json?per_page=0&page=3'
51 | expect(last_response.header['Link']).to eq('; rel="first", ; rel="prev", ; rel="next", ; rel="last"')
52 | end
53 |
54 | it "sets the correct HTTP X-Items-Count response header" do
55 | get 'resources.json'
56 | expect(last_response.header['X-Items-Count']).to eq('1201')
57 | end
58 |
59 | it "sets the correct HTTP X-Pages-Count response header" do
60 | get 'resources.json'
61 | expect(last_response.header['X-Pages-Count']).to eq('61')
62 |
63 | get 'resources.json?per_page=5&page=3'
64 | expect(last_response.header['X-Pages-Count']).to eq('241')
65 |
66 | get 'resources.json?per_page=5000&page=3'
67 | expect(last_response.header['X-Pages-Count']).to eq('13')
68 |
69 | get 'resources.json?per_page=0&page=3'
70 | expect(last_response.header['X-Pages-Count']).to eq('1201')
71 | end
72 | end
73 | end if defined?(Grape)
74 |
--------------------------------------------------------------------------------
/spec/api_helper/grape/sortable_spec.rb:
--------------------------------------------------------------------------------
1 | require 'grape_helper'
2 |
3 | describe APIHelper::Sortable do
4 | context "used in a Grape app" do
5 | include Rack::Test::Methods
6 |
7 | class SortableAPI < Grape::API
8 | helpers APIHelper::Sortable
9 |
10 | resources :resources do
11 | get do
12 | sortable(default_order: { string: :desc })
13 | collection = Model.order(sortable_sort)
14 | return collection
15 | end
16 | end
17 | end
18 |
19 | def app
20 | SortableAPI
21 | end
22 |
23 | it "sorts the resource with the given order" do
24 | get 'resources.json?sort=integer'
25 | json = JSON.parse(last_response.body)
26 |
27 | expect(json[0]['integer']).to eq(nil)
28 | expect(json[1]['integer']).to eq(nil)
29 | expect(json[2]['integer']).to eq(nil)
30 | expect(json[3]['integer']).to eq(1)
31 | expect(json[4]['integer']).to eq(2)
32 | expect(json[5]['integer']).to eq(3)
33 |
34 | get 'resources.json?sort=-integer'
35 | json = JSON.parse(last_response.body)
36 |
37 | expect(json[0]['integer']).to eq(5)
38 | expect(json[1]['integer']).to eq(5)
39 | expect(json[2]['integer']).to eq(5)
40 |
41 | get 'resources.json?sort=-integer,string'
42 | json = JSON.parse(last_response.body)
43 |
44 | expect(json[0]['integer']).to eq(5)
45 | expect(json[0]['string']).to eq('')
46 | expect(json[1]['integer']).to eq(5)
47 | expect(json[1]['string']).to eq('хорошо')
48 | expect(json[2]['integer']).to eq(5)
49 | expect(json[2]['string']).to eq('好!')
50 |
51 | get 'resources.json?sort=-integer,-string'
52 | json = JSON.parse(last_response.body)
53 |
54 | expect(json[0]['integer']).to eq(5)
55 | expect(json[0]['string']).to eq('好!')
56 | expect(json[1]['integer']).to eq(5)
57 | expect(json[1]['string']).to eq('хорошо')
58 | expect(json[2]['integer']).to eq(5)
59 | expect(json[2]['string']).to eq('')
60 | end
61 |
62 | it "sorts the resource with the default order" do
63 | get 'resources.json'
64 | json = JSON.parse(last_response.body)
65 |
66 | expect(json[0]['string']).to eq('好!')
67 | expect(json[1]['string']).to eq('хорошо')
68 | expect(json[2]['string']).to eq('yo')
69 | end
70 | end
71 | end if defined?(Grape)
72 |
--------------------------------------------------------------------------------
/spec/api_helper/includable_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe APIHelper::Includable do
4 | describe ".include_param_desc" do
5 | it "returns a string" do
6 | expect(APIHelper::Includable.include_param_desc).to be_a(String)
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/spec/api_helper/multigettable_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe APIHelper::Multigettable do
4 | end
5 |
--------------------------------------------------------------------------------
/spec/api_helper/paginatable_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe APIHelper::Paginatable do
4 | describe ".per_page_param_desc" do
5 | it "returns a string" do
6 | expect(APIHelper::Paginatable.per_page_param_desc).to be_a(String)
7 | end
8 | end
9 |
10 | describe ".page_param_desc" do
11 | it "returns a string" do
12 | expect(APIHelper::Paginatable.page_param_desc).to be_a(String)
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/spec/api_helper/rails/fieldsettable_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | describe APIHelper::Fieldsettable do
4 | context "used in a Rails controller", :type => :controller do
5 |
6 | context "with specified fieldsets" do
7 |
8 | controller(TestRailsApp::ApplicationController) do
9 | include APIHelper::Fieldsettable
10 |
11 | def index
12 | fieldset_for :post, default: true, default_fields: [:title, :content]
13 | fieldset_for :user
14 | render json: []
15 | end
16 | end
17 |
18 | it "parses the fieldsets for each resource" do
19 | # GET /?fields[post]=title,author&fields[user]=name
20 | get :index, fields: { post: 'title,author', user: 'name' }
21 |
22 | expect(controller.fieldset).to eq('post' => ['title', 'author'],
23 | 'user' => ['name'])
24 | expect(controller.fieldset[:post]).to eq(['title', 'author'])
25 | expect(controller.fieldset(:post)).to eq(['title', 'author'])
26 | expect(controller.fieldset(:post, :author)).to be true
27 | expect(controller.fieldset(:post, 'author')).to be true
28 | expect(controller.fieldset('post', :author)).to be true
29 | expect(controller.fieldset('post', 'author')).to be true
30 | expect(controller.fieldset(:post, :content)).to be false
31 |
32 | expect(controller.fieldset(:nothing)).to eq([])
33 | end
34 |
35 | it "parses the currect fieldsets for the default resource" do
36 | # GET /?fields=title,author
37 | get :index, fields: 'title,author'
38 |
39 | expect(controller.fieldset[:post]).to eq(['title', 'author'])
40 | expect(controller.fieldset(:post)).to eq(['title', 'author'])
41 | expect(controller.fieldset(:post, :author)).to be true
42 | expect(controller.fieldset(:post, 'author')).to be true
43 | expect(controller.fieldset('post', :author)).to be true
44 | expect(controller.fieldset('post', 'author')).to be true
45 | expect(controller.fieldset(:post, :content)).to be false
46 | end
47 |
48 | it "uses the default fieldsets if not specified" do
49 | # GET /
50 | get :index
51 |
52 | expect(controller.fieldset[:post]).to eq(['title', 'content'])
53 | expect(controller.fieldset(:post)).to eq(['title', 'content'])
54 | expect(controller.fieldset(:post, :content)).to be true
55 | expect(controller.fieldset(:user)).to be_blank
56 | end
57 |
58 | it "ignores undeclared resource" do
59 | # GET /?fields[post]=title,author&fields[ufo]=radius
60 | get :index, fields: { post: 'title,author', ufo: 'radius' }
61 |
62 | expect(controller.fieldset(:post)).to eq(['title', 'author'])
63 | expect(controller.fieldset(:ufo)).to be_blank
64 | end
65 |
66 | describe "view helpers" do
67 | describe "set_fieldset" do
68 | it "sets the properties of fieldset" do
69 | # GET /?fields[post]=title,author,secret
70 | get :index, fields: { post: 'title,author,secret' }
71 |
72 | # These methods are normally be called in the view
73 | controller.set_fieldset :post, permitted_fields: [:title, :content, :author]
74 | controller.set_fieldset :user, default_fields: [:avatar_url, :name]
75 |
76 | expect(controller.fieldset(:post)).to eq(['title', 'author'])
77 | expect(controller.fieldset(:user)).to eq(['avatar_url', 'name'])
78 | end
79 | end
80 | end
81 | end
82 |
83 | context "with specified permitted_fields fieldsets" do
84 |
85 | controller(TestRailsApp::ApplicationController) do
86 | include APIHelper::Fieldsettable
87 |
88 | def index
89 | fieldset_for :post, default: true,
90 | permitted_fields: [:title, :content, :author],
91 | default_fields: [:title, :content]
92 | fieldset_for :user, permitted_fields: [:id, :avatar_url, :name],
93 | defaults_to_permitted_fields: true
94 |
95 | render json: []
96 | end
97 | end
98 |
99 | it "does not return unpermitted fields for an resource" do
100 | # GET /?fields[post]=title,author,secret&fields[user]=name,password
101 | get :index, fields: { post: 'title,author,secret', user: 'name,password' }
102 |
103 | expect(controller.fieldset).to eq('post' => ['title', 'author'],
104 | 'user' => ['name'])
105 | expect(controller.fieldset(:user, :password)).to be false
106 | end
107 |
108 | it "uses the default fieldsets if not specified" do
109 | # GET /
110 | get :index
111 |
112 | expect(controller.fieldset(:user)).to eq(%w(id avatar_url name))
113 | end
114 | end
115 | end
116 | end if defined?(Rails)
117 |
--------------------------------------------------------------------------------
/spec/api_helper/rails/filterable_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | describe APIHelper::Filterable do
4 | context "used in a Rails controller", :type => :controller do
5 |
6 | controller(TestRailsApp::ApplicationController) do
7 | include APIHelper::Filterable
8 |
9 | def index
10 | collection = filter(Model.all)
11 | render json: collection
12 | end
13 | end
14 |
15 | it "filters out resource with a matching attribute" do
16 | # GET /?filter[string]=yo
17 | get :index, filter: { string: 'yo' }
18 | json = JSON.parse(response.body)
19 |
20 | expect(json).not_to be_blank
21 | json.each do |resource|
22 | expect(resource['string']).to eq('yo')
23 | end
24 |
25 | # GET /?filter[boolean]=true
26 | get :index, filter: { boolean: 'true' }
27 | json = JSON.parse(response.body)
28 |
29 | expect(json).not_to be_blank
30 | json.each do |resource|
31 | expect(resource['boolean']).to be true
32 | end
33 | end
34 |
35 | it "filters out resource with multiple matching attribute" do
36 | # GET /?filter[integer]=1,2,5,7
37 | get :index, filter: { integer: '1,2,5,7' }
38 | json = JSON.parse(response.body)
39 |
40 | expect(json).not_to be_blank
41 | json.each do |resource|
42 | expect([1, 2, 5, 7]).to include(resource['integer'])
43 | end
44 | end
45 |
46 | it "filters out resource with the \"not()\" function" do
47 | # GET /?filter[string]=not(yo,hi,boom)
48 | get :index, filter: { string: 'not(yo,hi,boom)' }
49 | json = JSON.parse(response.body)
50 |
51 | expect(json).not_to be_blank
52 | json.each do |resource|
53 | expect(resource['string']).not_to eq('yo')
54 | expect(resource['string']).not_to eq('hi')
55 | expect(resource['string']).not_to eq('boom')
56 | end
57 | end
58 |
59 | it "filters out resource with the \"greater_then()\" function" do
60 | # GET /?filter[integer]=greater_then(3)
61 | get :index, filter: { integer: 'greater_then(3)' }
62 | json = JSON.parse(response.body)
63 |
64 | expect(json).not_to be_blank
65 | json.each do |resource|
66 | expect(resource['integer']).to be > 3
67 | end
68 | end
69 |
70 | it "filters out resource with the \"less_then()\" function" do
71 | # GET /?filter[integer]=less_then(3)
72 | get :index, filter: { integer: 'less_then(3)' }
73 | json = JSON.parse(response.body)
74 |
75 | expect(json).not_to be_blank
76 | json.each do |resource|
77 | expect(resource['integer']).to be < 3
78 | end
79 | end
80 |
81 | it "filters out resource with the \"greater_then_or_equal()\" function" do
82 | # GET /?filter[integer]=greater_then_or_equal(3)
83 | get :index, filter: { integer: 'greater_then_or_equal(3)' }
84 | json = JSON.parse(response.body)
85 |
86 | expect(json).not_to be_blank
87 | json.each do |resource|
88 | expect(resource['integer']).to be >= 3
89 | end
90 | end
91 |
92 | it "filters out resource with the \"less_then_or_equal()\" function" do
93 | # GET /?filter[integer]=less_then_or_equal(3)
94 | get :index, filter: { integer: 'less_then_or_equal(3)' }
95 | json = JSON.parse(response.body)
96 |
97 | expect(json).not_to be_blank
98 | json.each do |resource|
99 | expect(resource['integer']).to be <= 3
100 | end
101 | end
102 |
103 | it "filters out resource with the \"between()\" function" do
104 | # GET /?filter[integer]=between(2,4)
105 | get :index, filter: { integer: 'between(2,4)' }
106 | json = JSON.parse(response.body)
107 |
108 | expect(json).not_to be_blank
109 | json.each do |resource|
110 | expect(resource['integer']).to be >= 2
111 | expect(resource['integer']).to be <= 4
112 | end
113 |
114 | # GET /?filter[datetime]=between(1970-1-1,2199-1-1)
115 | get :index, filter: { datetime: 'between(1970-1-1,2199-1-1)' }
116 | json = JSON.parse(response.body)
117 |
118 | expect(json).not_to be_blank
119 | json.each do |resource|
120 | expect(Time.new(resource['datetime'])).to be >= Time.new(1970, 1, 1)
121 | expect(Time.new(resource['datetime'])).to be <= Time.new(2199, 1, 1)
122 | end
123 | end
124 |
125 | it "filters out resource with the \"contains()\" function" do
126 | # GET /?filter[string]=contains(o)
127 | get :index, filter: { string: 'contains(o)' }
128 | json = JSON.parse(response.body)
129 |
130 | expect(json).not_to be_blank
131 | json.each do |resource|
132 | expect(resource['string']).to include('o')
133 | end
134 | end
135 |
136 | it "filters out resource with the \"null()\" function" do
137 | # GET /?filter[string]=null()
138 | get :index, filter: { string: 'null()' }
139 | json = JSON.parse(response.body)
140 |
141 | expect(json).not_to be_blank
142 | json.each do |resource|
143 | expect(resource['string']).to eq(nil)
144 | end
145 |
146 | # GET /?filter[boolean]=null()
147 | get :index, filter: { boolean: 'null()' }
148 | json = JSON.parse(response.body)
149 |
150 | expect(json).not_to be_blank
151 | json.each do |resource|
152 | expect(resource['boolean']).to eq(nil)
153 | end
154 | end
155 |
156 | it "filters out resource with the \"blank()\" function" do
157 | # GET /?filter[string]=blank()
158 | get :index, filter: { string: 'blank()' }
159 | json = JSON.parse(response.body)
160 |
161 | expect(json).not_to be_blank
162 | json.each do |resource|
163 | expect(resource['string']).to be_blank
164 | end
165 | end
166 |
167 | it "filters out resource with multiple conditions" do
168 | # GET /?filter[integer]=between(2,4)&filter[string]=contains(o)
169 | get :index, filter: { integer: 'between(2,4)', string: 'contains(o)' }
170 | json = JSON.parse(response.body)
171 |
172 | expect(json).not_to be_blank
173 | json.each do |resource|
174 | expect(resource['integer']).to be_between(2, 4)
175 | expect(resource['string']).to include('o')
176 | end
177 | end
178 |
179 | it "ignores filtering with unknown fields" do
180 | # GET /?filter[string]=хорошо&unknown_field=val
181 | get :index, filter: { string: 'хорошо', unknown_field: 'val' }
182 | json = JSON.parse(response.body)
183 |
184 | expect(json).not_to be_blank
185 | json.each do |resource|
186 | expect(resource['string']).to eq('хорошо')
187 | end
188 | end
189 |
190 | context "with filterable fields specified" do
191 |
192 | controller(TestRailsApp::ApplicationController) do
193 | include APIHelper::Filterable
194 |
195 | def index
196 | collection =
197 | filter(Model.all, filterable_fields: [:integer, :boolean])
198 | render json: collection
199 | end
200 | end
201 |
202 | it "is only filterable with filterable fields" do
203 | # GET /?filter[string]=хорошо&integer=5
204 | get :index, filter: { string: 'хорошо', integer: '5' }
205 | json = JSON.parse(response.body)
206 |
207 | expect(json).not_to be_blank
208 | expect(json.count).to be > 1
209 | end
210 | end
211 | end
212 | end if defined?(Rails)
213 |
--------------------------------------------------------------------------------
/spec/api_helper/rails/includable_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | describe APIHelper::Includable do
4 | context "used in a Rails controller", :type => :controller do
5 |
6 | controller(TestRailsApp::ApplicationController) do
7 | include APIHelper::Includable
8 |
9 | def index
10 | inclusion_for :post, default: true, default_includes: [:author, :comments]
11 | inclusion_for :user, permitted_includes: [:posts, :followers]
12 | render json: []
13 | end
14 | end
15 |
16 | it "parses the included fields for each resource" do
17 | # GET /?include[post]=author,comments,stargazers&include[user]=followers
18 | get :index, include: { post: 'author,comments,stargazers', user: 'followers' }
19 |
20 | expect(controller.inclusion).to eq('post' => ['author', 'comments', 'stargazers'],
21 | 'user' => ['followers'])
22 | expect(controller.inclusion[:post]).to eq(['author', 'comments', 'stargazers'])
23 | expect(controller.inclusion(:user)).to eq(['followers'])
24 | expect(controller.inclusion(:post, :author)).to be true
25 | expect(controller.inclusion(:post, 'author')).to be true
26 | expect(controller.inclusion('post', :author)).to be true
27 | expect(controller.inclusion('post', 'author')).to be true
28 | expect(controller.inclusion(:post, :board)).to be false
29 |
30 | expect(controller.inclusion(:nothing)).to eq([])
31 | end
32 |
33 | it "parses the currect fieldsets for the default resource" do
34 | # GET /?include=author,comments,stargazers
35 | get :index, include: 'author,comments,stargazers'
36 |
37 | expect(controller.inclusion[:post]).to eq(['author', 'comments', 'stargazers'])
38 | expect(controller.inclusion(:post)).to eq(['author', 'comments', 'stargazers'])
39 | expect(controller.inclusion(:post, :author)).to be true
40 | expect(controller.inclusion(:post, 'author')).to be true
41 | expect(controller.inclusion('post', :author)).to be true
42 | expect(controller.inclusion('post', 'author')).to be true
43 | expect(controller.inclusion(:post, :board)).to be false
44 | end
45 |
46 | it "uses the default fieldsets if not specified" do
47 | # GET /
48 | get :index
49 |
50 | expect(controller.inclusion[:post]).to eq(['author', 'comments'])
51 | expect(controller.inclusion(:post)).to eq(['author', 'comments'])
52 | expect(controller.inclusion(:post, :author)).to be true
53 | expect(controller.inclusion(:user)).to be_blank
54 | end
55 |
56 | it "ignores undeclared resource" do
57 | # GET /?include[post]=author&include[ufo]=radius
58 | get :index, include: { post: 'author', ufo: 'passengers' }
59 |
60 | expect(controller.inclusion(:post)).to eq(['author'])
61 | expect(controller.inclusion(:ufo)).to be_blank
62 | end
63 |
64 | context "permitted includes is set" do
65 | it "ignores unpermitted fields" do
66 | # GET /?include[post]=author&include[user]=followers,devices
67 | get :index, include: { post: 'author', user: 'followers,devices' }
68 |
69 | expect(controller.inclusion(:post)).to eq(['author'])
70 | expect(controller.inclusion(:user)).to eq(['followers'])
71 | expect(controller.inclusion(:user, :devices)).to be false
72 | end
73 |
74 | it "can be disabled while giving invalid parameters" do
75 | # GET /?include[user]=none
76 | get :index, include: { user: 'none' }
77 |
78 | expect(controller.inclusion(:user)).to be_blank
79 | end
80 | end
81 |
82 | context "integrated with fieldsettable" do
83 |
84 | controller(TestRailsApp::ApplicationController) do
85 | include APIHelper::Fieldsettable
86 | include APIHelper::Includable
87 |
88 | def index
89 | fieldset_for :post, default: true, default_fields: [:title, :author, :content]
90 | fieldset_for :user
91 | inclusion_for :post, default: true, default_includes: [:author, :comments]
92 | inclusion_for :user, permitted_includes: [:posts, :followers]
93 | render json: []
94 | end
95 | end
96 |
97 | it "ignores fields not listed in fieldset" do
98 | # GET /?include=author,comments
99 | get :index, include: 'author,comments'
100 |
101 | expect(controller.inclusion(:post)).to eq(['author'])
102 | expect(controller.inclusion(:post, :comments)).to be false
103 |
104 | # GET /?fields=title,author,comments&include=author,comments
105 | get :index, fields: 'title,author,comments',
106 | include: 'author,comments'
107 |
108 | expect(controller.inclusion(:post)).to eq(['author', 'comments'])
109 | expect(controller.inclusion(:post, :comments)).to be true
110 |
111 | expect(controller.fieldset(:post)).to eq(['title', 'author', 'comments'])
112 | end
113 |
114 | it "ignores default inclusion fields not listed in fieldset" do
115 | # GET /?fields=title,author,comments
116 | get :index, fields: 'title,author'
117 | # This method is normally be called in the view
118 | controller.set_inclusion :post,
119 | default_includes: [:author, :comments]
120 | expect(controller.inclusion(:post, :author)).to be true
121 | expect(controller.inclusion(:post, :comments)).to be false
122 | end
123 |
124 | it "includes fields by default" do
125 | # GET /?fields=title,author,comments
126 | get :index, fields: 'title,author,comments'
127 |
128 | expect(controller.inclusion(:post)).to eq(['author', 'comments'])
129 | expect(controller.inclusion(:post, :comments)).to be true
130 |
131 | expect(controller.fieldset(:post)).to eq(['title', 'author', 'comments'])
132 | end
133 | end
134 |
135 | describe "view helpers" do
136 | describe "set_inclusion" do
137 | it "sets the properties of inclusion" do
138 | # GET /?include[post]=author,comments,board
139 | get :index, include: { post: 'author,comments,board' }
140 |
141 | # This method is normally be called in the view
142 | controller.set_inclusion :post, default_includes: [:author],
143 | permitted_includes: [:author, :comments]
144 |
145 | expect(controller.inclusion(:post)).to eq(['author', 'comments'])
146 |
147 | # GET /
148 | get :index
149 |
150 | # This method is normally be called in the view
151 | controller.set_inclusion :user, default_includes: [:post]
152 |
153 | expect(controller.inclusion(:user)).to eq(['post'])
154 |
155 | # GET /?include=false
156 | get :index
157 |
158 | # This method is normally be called in the view
159 | controller.set_inclusion :post, default_includes: [:author],
160 | permitted_includes: [:author, :comments]
161 |
162 | expect(controller.inclusion(:post)).to be_blank
163 | end
164 | end
165 |
166 | describe "set_inclusion_field" do
167 | it "sets the properties of includable fields" do
168 | # These methods are normally called in the view
169 | controller.set_inclusion_field :post, :comments, :comment_ids,
170 | resource_name: :comment,
171 | resources_url: '/comments'
172 | controller.set_inclusion_field :post, :author, :author_id,
173 | resource_name: :user,
174 | resources_url: '/users'
175 |
176 | expect(controller.instance_variable_get(:'@inclusion_field')).to \
177 | eq('post' => {
178 | 'comments' => {
179 | 'field' => :comments,
180 | 'id_field' => :comment_ids,
181 | 'resource_name' => :comment,
182 | 'resources_url' => '/comments'
183 | },
184 | 'author' => {
185 | 'field' => :author,
186 | 'id_field' => :author_id,
187 | 'resource_name' => :user,
188 | 'resources_url' => '/users'
189 | }
190 | })
191 | end
192 | end
193 |
194 | describe "inclusion_field" do
195 | it "get the properties of includable fields" do
196 | # These methods are normally called in the view
197 | controller.set_inclusion_field :post, :comments, :comment_ids,
198 | resource_name: :comment,
199 | resources_url: '/comments'
200 | controller.set_inclusion_field :post, :author, :author_id,
201 | resource_name: :user,
202 | resources_url: '/users'
203 |
204 | expect(controller.inclusion_field).to \
205 | eq('post' => {
206 | 'comments' => {
207 | 'field' => :comments,
208 | 'id_field' => :comment_ids,
209 | 'resource_name' => :comment,
210 | 'resources_url' => '/comments'
211 | },
212 | 'author' => {
213 | 'field' => :author,
214 | 'id_field' => :author_id,
215 | 'resource_name' => :user,
216 | 'resources_url' => '/users'
217 | }
218 | })
219 |
220 | expect(controller.inclusion_field(:post)).to \
221 | eq('comments' => {
222 | 'field' => :comments,
223 | 'id_field' => :comment_ids,
224 | 'resource_name' => :comment,
225 | 'resources_url' => '/comments'
226 | },
227 | 'author' => {
228 | 'field' => :author,
229 | 'id_field' => :author_id,
230 | 'resource_name' => :user,
231 | 'resources_url' => '/users'
232 | })
233 |
234 | expect(controller.inclusion_field(:nothing)).to eq({})
235 | end
236 | end
237 | end
238 | end
239 | end if defined?(Rails)
240 |
--------------------------------------------------------------------------------
/spec/api_helper/rails/multigettable_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | describe APIHelper::Multigettable do
4 | context "used in a Rails controller", :type => :controller do
5 |
6 | controller(TestRailsApp::ApplicationController) do
7 | include APIHelper::Multigettable
8 |
9 | def index
10 | collection = multiget(Model.all)
11 | render json: collection
12 | end
13 | end
14 |
15 | it "can get a specified resource" do
16 | # GET /1
17 | get :index, id: 1
18 | json = JSON.parse(response.body)
19 |
20 | expect(json['id']).to eq(1)
21 |
22 | # GET /2
23 | get :index, id: 2
24 | json = JSON.parse(response.body)
25 |
26 | expect(json['id']).to eq(2)
27 |
28 | expect(controller.multiget?).to be false
29 | end
30 |
31 | it "can get multiple specified resource" do
32 | # GET /1,4,2,5
33 | get :index, id: '1,4,2,5'
34 | json = JSON.parse(response.body)
35 |
36 | expect(json.count).to eq(4)
37 | json.each do |resource|
38 | expect([1, 4, 2, 5]).to include(resource['id'])
39 | end
40 |
41 | expect(controller.multiget?).to be true
42 | end
43 |
44 | context "with maximum resource count limitations" do
45 |
46 | controller(TestRailsApp::ApplicationController) do
47 | include APIHelper::Multigettable
48 |
49 | def index
50 | collection = multiget(Model.all, max: 3)
51 | render json: collection
52 | end
53 | end
54 |
55 | it "limits the maximum resources returned" do
56 | # GET /1,4,2,5
57 | get :index, id: '1,4,2,5'
58 | json = JSON.parse(response.body)
59 |
60 | expect(json.count).to eq(3)
61 | end
62 | end
63 |
64 | context "with custom ids" do
65 |
66 | controller(TestRailsApp::ApplicationController) do
67 | include APIHelper::Multigettable
68 |
69 | def index
70 | collection = multiget(Model.all, find_by: :string, param: :code)
71 | render json: collection
72 | end
73 | end
74 |
75 | it "can get a specified resource" do
76 | get :index, code: 'yo'
77 | json = JSON.parse(response.body)
78 |
79 | expect(json['string']).to eq('yo')
80 |
81 | expect(controller.multiget?).to be false
82 | expect(controller.multiget?(param: :code)).to be false
83 | end
84 |
85 | it "can get multiple specified resource" do
86 | get :index, code: 'hi,yo'
87 | json = JSON.parse(response.body)
88 |
89 | expect(json.count).to eq(2)
90 | json.each do |resource|
91 | expect(%w(hi yo)).to include(resource['string'])
92 | end
93 |
94 | expect(controller.multiget?).to be false
95 | expect(controller.multiget?(param: :code)).to be true
96 | end
97 | end
98 | end
99 | end if defined?(Rails)
100 |
--------------------------------------------------------------------------------
/spec/api_helper/rails/paginatable_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | describe APIHelper::Paginatable do
4 | context "used in a Rails controller", :type => :controller do
5 |
6 | controller(TestRailsApp::ApplicationController) do
7 | include APIHelper::Paginatable
8 |
9 | def index
10 | pagination 1201, default_per_page: 20, maxium_per_page: 100
11 | render json: []
12 | end
13 | end
14 |
15 | it "sets the correct HTTP Link response header" do
16 | # GET /
17 | get :index
18 | expect(response.header['Link']).to eq('; rel="next", ; rel="last"')
19 |
20 | # GET /?page=2
21 | get :index, page: 2
22 | expect(response.header['Link']).to eq('; rel="first", ; rel="prev", ; rel="next", ; rel="last"')
23 |
24 | # GET /?page=3
25 | get :index, page: 3
26 | expect(response.header['Link']).to eq('; rel="first", ; rel="prev", ; rel="next", ; rel="last"')
27 |
28 | # GET /?page=60
29 | get :index, page: 60
30 | expect(response.header['Link']).to eq('; rel="first", ; rel="prev", ; rel="next", ; rel="last"')
31 |
32 | # GET /?page=61
33 | get :index, page: 61
34 | expect(response.header['Link']).to eq('; rel="first", ; rel="prev"')
35 |
36 | # GET /?page=62
37 | get :index, page: 62
38 | expect(response.header['Link']).to eq('; rel="first", ; rel="prev"')
39 |
40 | # GET /?page=0
41 | get :index, page: 0
42 | expect(response.header['Link']).to eq('; rel="next", ; rel="last"')
43 |
44 | # GET /?per_page=5&page=3
45 | # get :index, per_page: 5, page: 3
46 | # expect(response.header['Link']).to eq('; rel="first", ; rel="prev", ; rel="next", ; rel="last"')
47 |
48 | # GET /?per_page=5000&page=3
49 | get :index, per_page: 5000, page: 3
50 | # expect(response.header['Link']).to eq('; rel="first", ; rel="prev", ; rel="next", ; rel="last"')
51 |
52 | # GET /?per_page=0&page=3
53 | get :index, per_page: 0, page: 3
54 | # expect(response.header['Link']).to eq('; rel="first", ; rel="prev", ; rel="next", ; rel="last"')
55 | end
56 |
57 | it "sets the correct HTTP X-Items-Count response header" do
58 | # GET /
59 | get :index
60 | expect(response.header['X-Items-Count']).to eq('1201')
61 | end
62 |
63 | it "sets the correct HTTP X-Pages-Count response header" do
64 | # GET /
65 | get :index
66 | expect(response.header['X-Pages-Count']).to eq('61')
67 |
68 | # GET /?per_page=5&page=3
69 | get :index, per_page: 5, page: 3
70 | expect(response.header['X-Pages-Count']).to eq('241')
71 |
72 | # GET /?per_page=5000&page=3
73 | get :index, per_page: 5000, page: 3
74 | expect(response.header['X-Pages-Count']).to eq('13')
75 |
76 | # GET /?per_page=0&page=3
77 | get :index, per_page: 0, page: 3
78 | expect(response.header['X-Pages-Count']).to eq('1201')
79 | end
80 |
81 | describe "helper methods" do
82 | describe "pagination_per_page" do
83 | it "returns page of the request" do
84 | # GET /
85 | get :index
86 | expect(controller.pagination_per_page).to eq(20)
87 |
88 | # GET /?page=3
89 | get :index, page: 3
90 | expect(controller.pagination_per_page).to eq(20)
91 |
92 | # GET /?page=25
93 | get :index, per_page: 25
94 | expect(controller.pagination_per_page).to eq(25)
95 |
96 | # GET /?per_page=5000
97 | get :index, per_page: 5000
98 | expect(controller.pagination_per_page).to eq(100)
99 |
100 | # GET /?per_page=0
101 | get :index, per_page: 0
102 | expect(controller.pagination_per_page).to eq(1)
103 | end
104 | end
105 |
106 | describe "pagination_page" do
107 | it "returns page of the request" do
108 | # GET /
109 | get :index
110 | expect(controller.pagination_page).to eq(1)
111 |
112 | # GET /?page=3
113 | get :index, page: 3
114 | expect(controller.pagination_page).to eq(3)
115 |
116 | # GET /?page=0
117 | get :index, page: 0
118 | expect(controller.pagination_page).to eq(1)
119 |
120 | # GET /?per_page=5000&page=3
121 | get :index, per_page: 5000, page: 14
122 | expect(controller.pagination_page).to eq(13)
123 | end
124 | end
125 | end
126 | end
127 | end if defined?(Rails)
128 |
--------------------------------------------------------------------------------
/spec/api_helper/rails/sortable_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | describe APIHelper::Sortable do
4 | context "used in a Rails controller", :type => :controller do
5 |
6 | controller(TestRailsApp::ApplicationController) do
7 | include APIHelper::Sortable
8 |
9 | def index
10 | sortable(default_order: { string: :desc })
11 | collection = Model.order(sortable_sort)
12 | render json: collection
13 | end
14 | end
15 |
16 | it "sorts the resource with the given order" do
17 | # GET /?sort=integer
18 | get :index, sort: 'integer'
19 | json = JSON.parse(response.body)
20 |
21 | expect(json[0]['integer']).to eq(nil)
22 | expect(json[1]['integer']).to eq(nil)
23 | expect(json[2]['integer']).to eq(nil)
24 | expect(json[3]['integer']).to eq(1)
25 | expect(json[4]['integer']).to eq(2)
26 | expect(json[5]['integer']).to eq(3)
27 |
28 | # GET /?sort=-integer
29 | get :index, sort: '-integer'
30 | json = JSON.parse(response.body)
31 |
32 | expect(json[0]['integer']).to eq(5)
33 | expect(json[1]['integer']).to eq(5)
34 | expect(json[2]['integer']).to eq(5)
35 |
36 | # GET /?sort=-integer,string
37 | get :index, sort: '-integer,string'
38 | json = JSON.parse(response.body)
39 |
40 | expect(json[0]['integer']).to eq(5)
41 | expect(json[0]['string']).to eq('')
42 | expect(json[1]['integer']).to eq(5)
43 | expect(json[1]['string']).to eq('хорошо')
44 | expect(json[2]['integer']).to eq(5)
45 | expect(json[2]['string']).to eq('好!')
46 |
47 | # GET /?sort=-integer,-string
48 | get :index, sort: '-integer,-string'
49 | json = JSON.parse(response.body)
50 |
51 | expect(json[0]['integer']).to eq(5)
52 | expect(json[0]['string']).to eq('好!')
53 | expect(json[1]['integer']).to eq(5)
54 | expect(json[1]['string']).to eq('хорошо')
55 | expect(json[2]['integer']).to eq(5)
56 | expect(json[2]['string']).to eq('')
57 | end
58 |
59 | it "sorts the resource with the default order" do
60 | # GET /
61 | get :index
62 | json = JSON.parse(response.body)
63 |
64 | expect(json[0]['string']).to eq('好!')
65 | expect(json[1]['string']).to eq('хорошо')
66 | expect(json[2]['string']).to eq('yo')
67 | end
68 | end
69 | end if defined?(Rails)
70 |
--------------------------------------------------------------------------------
/spec/api_helper/sortable_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe APIHelper::Sortable do
4 | describe ".sort_param_desc" do
5 | it "returns a string" do
6 | expect(APIHelper::Sortable.sort_param_desc).to be_a(String)
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/spec/api_helper_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe APIHelper do
4 | it 'has a version number' do
5 | expect(APIHelper::VERSION).not_to be nil
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/grape_helper.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'json'
3 | require 'byebug'
4 |
5 | begin
6 | require 'grape'
7 | require 'rack/test'
8 | rescue LoadError
9 | end
10 |
--------------------------------------------------------------------------------
/spec/rails_helper.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | begin
4 | require 'rails'
5 | require 'action_controller/railtie'
6 | require 'rspec/rails'
7 | rescue LoadError
8 | else
9 | module TestRailsApp
10 | class Application < Rails::Application
11 | config.secret_token = SecureRandom.hex
12 | config.secret_key_base = SecureRandom.hex
13 | end
14 |
15 | class ApplicationController < ActionController::Base
16 | extend RSpec::Rails::ControllerExampleGroup::BypassRescue
17 | include Rails.application.routes.url_helpers
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/spec/shared_context/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zetavg/api_helper/05c1496f966be643a1f1390fa31598e286d6a411/spec/shared_context/.keep
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require 'simplecov'
2 |
3 | SimpleCov.profiles.define :gem do
4 | add_filter '/test/'
5 | add_filter '/features/'
6 | add_filter '/spec/'
7 | add_filter '/autotest/'
8 |
9 | add_group 'Binaries', '/bin/'
10 | add_group 'Libraries', '/lib/'
11 | add_group 'Extensions', '/ext/'
12 | add_group 'Vendor Libraries', '/vendor/'
13 | end
14 |
15 | begin
16 | require 'rails'
17 | rescue LoadError
18 | else
19 | require 'coveralls'
20 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
21 | SimpleCov::Formatter::HTMLFormatter,
22 | Coveralls::SimpleCov::Formatter
23 | ]
24 | end
25 |
26 | SimpleCov.start :gem
27 |
28 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
29 | require 'api_helper'
30 |
31 | Dir[File.expand_path('../support/**/*.rb', __FILE__)].each { |f| require f }
32 | Dir[File.expand_path('../shared_context/**/*.rb', __FILE__)].each { |f| require f }
33 |
34 | require 'byebug'
35 |
--------------------------------------------------------------------------------
/spec/support/active_record.rb:
--------------------------------------------------------------------------------
1 | require 'active_record'
2 |
3 | class ActiveRecord::Base
4 | establish_connection adapter: 'sqlite3',
5 | database: 'tmp/db.sqlite3'
6 | end
7 |
8 | class Model < ActiveRecord::Base
9 | end
10 |
11 | table_name = SecureRandom.hex
12 | Model.table_name = table_name
13 |
14 | migration = ActiveRecord::Migration.new
15 |
16 | migration.create_table table_name do |t|
17 | t.integer 'integer'
18 | t.string 'string'
19 | t.datetime 'datetime'
20 | t.boolean 'boolean'
21 | t.text 'text'
22 | end
23 |
24 | integers = [1, 2, 3, 4, 5, 5, 5]
25 | strings = ['yo', 'hi', 'hello', 'hola', '好!', 'хорошо', '']
26 | datetimes = [DateTime.new(1900, 1, 1), DateTime.new(2000, 1, 1), DateTime.new(2100, 1, 1)]
27 | booleans = [true, false]
28 | texts = %w(yo hi hello)
29 |
30 | 10.times do |i|
31 | m = Model.new(integer: integers[i],
32 | string: strings[i],
33 | datetime: datetimes[i],
34 | boolean: booleans[i],
35 | text: texts[i])
36 | m.save!
37 | end
38 |
--------------------------------------------------------------------------------