api_helper 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 991d12decd2615a223eebef7da4c5f132c47dfe6
4
+ data.tar.gz: 9bafcbeafb95c36abf2f5b091cec2b995b47a43e
5
+ SHA512:
6
+ metadata.gz: 78f5748409ee50cb19af1c70f9e73a35fb82dd72caff6d3b3009394ed7208c092525805bd6b41cd476693dd48243ccaca9bd4f6c003889cbf6614318dae16799
7
+ data.tar.gz: 11e33204b137a30a922d8c57d603d517c176f3bdce2f6cb3420fea31de524fb151c1348c55a2df32d330016eab88879b6e3eb3a4f2e7e776048b7de00bfd3603
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.0
4
+ before_install: gem install bundler -v 1.10.2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in api_helper.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # APIHelper
2
+
3
+ Helpers for creating standard RESTful API for Rails or Grape with Active Record.
4
+
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'APIHelper'
12
+ ```
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install APIHelper
21
+
22
+
23
+ ## API Standards
24
+
25
+ <dl>
26
+
27
+ <dt><a href="http://www.rubydoc.info/github/Neson/api_helper/master/APIHelper/Fieldsettable" target="_blank">Fieldsettable</a></dt>
28
+ <dd>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.</dd>
29
+
30
+ <dt><a href="http://www.rubydoc.info/github/Neson/api_helper/master/APIHelper/Includable" target="_blank">Includable</a></dt>
31
+ <dd>Clients can use the "include" query parameter to enable inclusion of related items - for instance, get the author's data along with a post.</dd>
32
+
33
+ <dt><a href="http://www.rubydoc.info/github/Neson/api_helper/master/APIHelper/Paginatable" target="_blank">Paginatable</a></dt>
34
+ <dd>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.</dd>
35
+
36
+ <dt><a href="http://www.rubydoc.info/github/Neson/api_helper/master/APIHelper/Sortable" target="_blank">Sortable</a></dt>
37
+ <dd>Client can set custom sorting with the "sort" query parameter while getting a resource collection.</dd>
38
+
39
+ <dt><a href="http://www.rubydoc.info/github/Neson/api_helper/master/APIHelper/Filterable" target="_blank">Filterable</a></dt>
40
+ <dd>Enables clients to filter through a resource collection with their fields.</dd>
41
+
42
+ <dt><a href="http://www.rubydoc.info/github/Neson/api_helper/master/APIHelper/Multigettable" target="_blank">Multigettable</a></dt>
43
+ <dd>Let Client execute operations on multiple resources with a single request.</dd>
44
+
45
+ </dl>
46
+
47
+
48
+ ## Usage
49
+
50
+ ### Ruby on Rails (Action Pack)
51
+
52
+ Include each helper concern you need in an `ActionController::Base`:
53
+
54
+ ```ruby
55
+ PostsController < ApplicationController
56
+ include APIHelpers::Filterable
57
+ include APIHelpers::Paginatable
58
+ include APIHelpers::Sortable
59
+
60
+ # ...
61
+
62
+ end
63
+ ```
64
+
65
+ Further usage of each helper can be found in the docs.
66
+
67
+ ### Grape
68
+
69
+ Set the helpers you need in an `Grape::API`:
70
+
71
+ ```ruby
72
+ class PostsAPI < Grape::API
73
+ helpers APIHelpers::Filterable
74
+ helpers APIHelpers::Paginatable
75
+ helpers APIHelpers::Sortable
76
+
77
+ # ...
78
+
79
+ end
80
+ ```
81
+
82
+ Further usage of each helper can be found in the docs.
83
+
84
+
85
+ ## Development
86
+
87
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
88
+
89
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
90
+
91
+
92
+ ## Contributing
93
+
94
+ Bug reports and pull requests are welcome on GitHub at https://github.com/Neson/APIHelper.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,26 @@
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 "rspec"
24
+
25
+ spec.add_development_dependency "activesupport", ">= 3"
26
+ end
data/bin/console ADDED
@@ -0,0 +1,14 @@
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
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,171 @@
1
+ require 'active_support'
2
+
3
+ # = Helper To Make Resource APIs Fieldsettable
4
+ #
5
+ # By making an API fieldsettable, you let API callers to choose the fields they
6
+ # wanted to be returned with query parameters. This is really useful for making
7
+ # API calls more efficient and fast.
8
+ #
9
+ # This design made references to the rules of <em>Sparse Fieldsets</em> in
10
+ # <em>JSON API</em>:
11
+ # http://jsonapi.org/format/#fetching-sparse-fieldsets
12
+ #
13
+ # A client can request that an API endpoint return only specific fields in the
14
+ # response by including a +fields+ parameter, which is a comma-separated (",")
15
+ # list that refers to the name(s) of the fields to be returned.
16
+ #
17
+ # GET /users?fields=id,name,avatar_url
18
+ #
19
+ # This functionality may also support requests specifying multiple fieldsets
20
+ # for several objects at a time (e.g. another object included in an field of
21
+ # another object) with <tt>fields[object_type]</tt> parameters.
22
+ #
23
+ # GET /posts?fields[posts]=id,title,author&fields[user]=id,name,avatar_url
24
+ #
25
+ # Note: +author+ of a +post+ is a +user+.
26
+ #
27
+ # The +fields+ and <tt>fields[object_type]</tt> parameters can not be mixed.
28
+ # If the latter format is used, then it must be used for the main object as well.
29
+ #
30
+ # == Usage
31
+ #
32
+ # Include this +Concern+ in your Action Controller:
33
+ #
34
+ # SamplesController < ApplicationController
35
+ # include APIHelper::Fieldsettable
36
+ # end
37
+ #
38
+ # or in your Grape API class:
39
+ #
40
+ # class SampleAPI < Grape::API
41
+ # include APIHelper::Fieldsettable
42
+ # end
43
+ #
44
+ # then set the options for the fieldset in the grape method:
45
+ #
46
+ # resources :posts do
47
+ # get do
48
+ # fieldset_for :post, root: true, default_fields: [:id, :title, :author]
49
+ # fieldset_for :user, permitted_fields: [:id, :name, :posts, :avatar_url],
50
+ # show_all_permitted_fields_by_default: true
51
+ # # ...
52
+ # end
53
+ # end
54
+ #
55
+ # This helper parses the +fields+ and <tt>fields[object_type]</tt> parameters to
56
+ # determine what the API caller wants, and save the results into instance
57
+ # variables for further usage.
58
+ #
59
+ # After this you can use the +fieldset+ helper method to get the fieldset data
60
+ # that the request specifies.
61
+ #
62
+ # With <tt>GET /posts?fields=title,author</tt>:
63
+ #
64
+ # fieldset #=> { post: [:title, :author], user: [:id, :name, :posts, :avatar_url] }
65
+ #
66
+ # With <tt>GET /posts?fields[post]=title,author&fields[user]=name</tt>:
67
+ #
68
+ # fieldset #=> { post: [:title, :author], user: [:name] }
69
+ # fieldset(:post) #=> [:title, :author]
70
+ # fieldset(:post, :title) #=> true
71
+ # fieldset(:user, :avatar_url) #=> false
72
+ #
73
+ # You can make use of the information while dealing with requests, for example:
74
+ #
75
+ # Post.select(fieldset(:post))...
76
+ #
77
+ # If you're using RABL as the API view, it can be also setup like this:
78
+ #
79
+ # object @user
80
+ #
81
+ # # this ensures the +fieldset+ instance variable is least setted with
82
+ # # the default fields, and double check +permitted_fields+ at view layer -
83
+ # # in case of things going wrong in the controller
84
+ # set_fieldset :user, default_fields: [:id, :name, :avatar_url],
85
+ # permitted_fields: [:id, :name, :avatar_url, :posts]
86
+ #
87
+ # # determine the fields to show on the fly
88
+ # attributes(*fieldset[:user])
89
+ module APIHelper::Fieldsettable
90
+ extend ActiveSupport::Concern
91
+
92
+ # Gets the fields parameters, organize them into a +@fieldset+ hash for model to select certain
93
+ # fields and/or templates to render specified fieldset. Following the URL rules of JSON API:
94
+ # http://jsonapi.org/format/#fetching-sparse-fieldsets
95
+ #
96
+ # Params:
97
+ #
98
+ # +resource+::
99
+ # +Symbol+ name of resource to receive the fieldset
100
+ #
101
+ # +root+::
102
+ # +Boolean+ should this resource take the parameter from +fields+ while no type is specified
103
+ #
104
+ # +permitted_fields+::
105
+ # +Array+ of +Symbol+s list of accessible fields used to filter out unpermitted fields,
106
+ # defaults to permit all
107
+ #
108
+ # +default_fields+::
109
+ # +Array+ of +Symbol+s list of fields to show by default
110
+ #
111
+ # +show_all_permitted_fields_by_default+::
112
+ # +Boolean+ if set to true, @fieldset will be set to all permitted_fields when the current
113
+ # resource's fieldset isn't specified
114
+ #
115
+ # Example Result:
116
+ #
117
+ # fieldset_for :user, root: true
118
+ # fieldset_for :group
119
+ #
120
+ # # @fieldset => {
121
+ # # :user => [:id, :name, :email, :groups],
122
+ # # :group => [:id, :name]
123
+ # # }
124
+ def fieldset_for(resource, root: false, permitted_fields: [], show_all_permitted_fields_by_default: false, default_fields: [])
125
+ @fieldset ||= Hashie::Mash.new
126
+ @meta ||= Hashie::Mash.new
127
+
128
+ # put the fields in place
129
+ if params[:fields].is_a? Hash
130
+ @fieldset[resource] = params[:fields][resource] || params[:fields][resource]
131
+ elsif root
132
+ @fieldset[resource] = params[:fields]
133
+ end
134
+
135
+ # splits the string into array of symbles
136
+ @fieldset[resource] = @fieldset[resource].present? ? @fieldset[resource].split(',').map(&:to_sym) : default_fields
137
+
138
+ # filter out unpermitted fields by intersecting them
139
+ @fieldset[resource] &= permitted_fields if @fieldset[resource].present? && permitted_fields.present?
140
+
141
+ # set default fields to permitted_fields if needed
142
+ @fieldset[resource] = permitted_fields if show_all_permitted_fields_by_default && @fieldset[resource].blank? && permitted_fields.present?
143
+ end
144
+
145
+ # View Helper to set the default and permitted fields
146
+ def set_fieldset(resource, default_fields: [], permitted_fields: [])
147
+ @fieldset ||= {}
148
+ @fieldset[resource] = default_fields if @fieldset[resource].blank?
149
+ @fieldset[resource] &= permitted_fields
150
+ end
151
+
152
+ # Getter for the fieldset data
153
+ def fieldset(resource = nil, field = nil)
154
+ if resource.blank?
155
+ @fieldset ||= {}
156
+ elsif field.blank?
157
+ (@fieldset ||= {})[resource] ||= []
158
+ else
159
+ fieldset(resource).include?(field)
160
+ end
161
+ end
162
+
163
+ # Return the 'fields' param description
164
+ def self.fields_param_desc(example: nil)
165
+ if example.present?
166
+ "Choose the fields to be returned. Example value: '#{example}'"
167
+ else
168
+ "Choose the fields to be returned."
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,128 @@
1
+ require 'active_support'
2
+
3
+ # = Helper To Make Resource APIs Filterable
4
+ #
5
+ # A filterable resource API supports requests to filter resources according to
6
+ # specific criteria, using the +filter+ query parameter.
7
+ #
8
+ # For example, the following is a request for all products that has a
9
+ # particular color:
10
+ #
11
+ # GET /products?filter[color]=red
12
+ #
13
+ # With this approach, multiple filters can be applied to a single request:
14
+ #
15
+ # GET /products?filter[color]=red&filter[status]=in-stock
16
+ #
17
+ # <em>Multiple filters are applied with the AND condition.</em>
18
+ #
19
+ # OR conditions of a single value can be represented as:
20
+ #
21
+ # GET /products?filter[color]=red,blue,yellow
22
+ #
23
+ # A few functions: +not+, +greater_then+, +less_then+, +greater_then_or_equal+,
24
+ # +less_then_or_equal+, +between+ and +like+ can be used while filtering
25
+ # the data, for example:
26
+ #
27
+ # GET /products?filter[color]=not(red)
28
+ # GET /products?filter[price]=greater_then(1000)
29
+ # GET /products?filter[price]=less_then_or_equal(2000)
30
+ # GET /products?filter[price]=between(1000,2000)
31
+ # GET /products?filter[name]=like(%lovely%)
32
+ #
33
+ # == Usage
34
+ #
35
+ # Include this +Concern+ in your Action Controller:
36
+ #
37
+ # SamplesController < ApplicationController
38
+ # include APIHelpers::Filterable
39
+ # end
40
+ #
41
+ # or in your Grape API class:
42
+ #
43
+ # class SampleAPI < Grape::API
44
+ # include APIHelper::Filterable
45
+ # end
46
+ #
47
+ # then use the +filter+ method like this:
48
+ #
49
+ # resources :products do
50
+ # get do
51
+ # @products = filter(Post, filterable_fields: [:name, :price, :color])
52
+ # # ...
53
+ # end
54
+ # end
55
+ #
56
+ # <em>The +filter+ method will return the scoped model, based directly
57
+ # from the requested URL.</em>
58
+ module APIHelper::Filterable
59
+ extend ActiveSupport::Concern
60
+
61
+ # Filter resources of a collection from the request parameter
62
+ #
63
+ # Params:
64
+ #
65
+ # +resource+::
66
+ # +ActiveRecord::Base+ or +ActiveRecord::Relation+ resource collection
67
+ # to filter data from
68
+ #
69
+ # +filterable_fields+::
70
+ # +Array+ of +Symbol+s fields that are allowed to be filtered, default
71
+ # to all
72
+ def filter(resource, filterable_fields: [])
73
+ # parse the request parameter
74
+ if params[:filter].is_a? Hash
75
+ @filter = params[:filter]
76
+ filterable_fields = filterable_fields.map(&:to_s)
77
+
78
+ @filter.each_pair do |field, condition|
79
+ next if filterable_fields.present? && !filterable_fields.include?(field)
80
+ field = resource.connection.quote_string(field)
81
+
82
+ next if resource.columns_hash[field].blank?
83
+ field_type = resource.columns_hash[field].type
84
+
85
+ # if a function is used
86
+ if func = condition.match(/(?<function>[^\(\)]+)\((?<param>.*)\)/)
87
+ case func[:function]
88
+ when 'not'
89
+ values = func[:param].split(',')
90
+ values.map!(&:to_bool) if field_type == :boolean
91
+ resource = resource.where.not(field => values)
92
+ when 'greater_then'
93
+ resource = resource.where("\"#{resource.table_name}\".\"#{field}\" > ?", func[:param])
94
+ when 'less_then'
95
+ resource = resource.where("\"#{resource.table_name}\".\"#{field}\" < ?", func[:param])
96
+ when 'greater_then_or_equal'
97
+ resource = resource.where("\"#{resource.table_name}\".\"#{field}\" >= ?", func[:param])
98
+ when 'less_then_or_equal'
99
+ resource = resource.where("\"#{resource.table_name}\".\"#{field}\" <= ?", func[:param])
100
+ when 'between'
101
+ param = func[:param].split(',')
102
+ resource = resource.where("\"#{resource.table_name}\".\"#{field}\" BETWEEN ? AND ?", param.first, param.last)
103
+ when 'like'
104
+ resource = resource.where("\"#{resource.table_name}\".\"#{field}\" LIKE ?", func[:param])
105
+ when 'null'
106
+ resource = resource.where(field => nil)
107
+ end
108
+ # if not function
109
+ else
110
+ values = condition.split(',')
111
+ values.map!(&:to_bool) if field_type == :boolean
112
+ resource = resource.where(field => values)
113
+ end
114
+ end
115
+ end
116
+
117
+ return resource
118
+ end
119
+
120
+ # Return the 'fields' param description
121
+ def self.filter_param_desc(for_field: nil)
122
+ if for_field.present?
123
+ "Filter data base on the '#{for_field}' field."
124
+ else
125
+ "Filter the data."
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,207 @@
1
+ require 'active_support'
2
+
3
+ # = Helper To Make Resource APIs Includable
4
+ #
5
+ # Inclusion of related resource lets your API return resources related to the
6
+ # primary data. This endpoint will support an +include+ request parameter to
7
+ # allow the client to customize which related resources should be returned.
8
+ #
9
+ # This design made references to the rules of <em>Inclusion of Related
10
+ # Resources</em> in <em>JSON API</em>:
11
+ # http://jsonapi.org/format/#fetching-includes
12
+ #
13
+ # For instance, comments could be requested with articles:
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:
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
+ # If requesting multiple related resources is needed, they can be stated in a
78
+ # comma-separated list:
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
+ # include APIHelper::Includable
94
+ # end
95
+ #
96
+ # then set the options for the inclusion in the grape method:
97
+ #
98
+ # resources :posts do
99
+ # get do
100
+ # inclusion_for :post, root: true
101
+ # # ...
102
+ # end
103
+ # end
104
+ #
105
+ # This helper parses the +include+ and <tt>include[object_type]</tt> parameters to
106
+ # determine what the API caller wants, and save the results into instance
107
+ # variables for further usage.
108
+ #
109
+ # After this you can use the +inclusion+ helper method to get the inclusion data
110
+ # that the request specifies, and do something like this in your controller:
111
+ #
112
+ # resource = resource.includes(:author) if inclusion(:post, :author)
113
+ #
114
+ # The +inclusion+ helper method returns data like this:
115
+ #
116
+ # inclusion #=> { post: [:author] }
117
+ # inclusion(:post) #=> [:author]
118
+ # inclusion(:post, :author) #=> true
119
+ #
120
+ # === API View with RABL
121
+ #
122
+ # If you're using RABL as the API view, it can be setup like this:
123
+ #
124
+ # # set the includable and default inclusion fields of the view
125
+ # set_inclusion :post, default_includes: [:author]
126
+ #
127
+ # # set the details for all includable fields
128
+ # set_inclusion_field :post, :author, :author_id
129
+ #
130
+ # # extends the partial to show included fields
131
+ # extends('extensions/includable_childs', locals: { self_resource: :post })
132
+ module APIHelper::Includable
133
+ extend ActiveSupport::Concern
134
+
135
+ # Gets the include parameters, organize them into a +@inclusion+ hash for model to use
136
+ # inner-join queries and/or templates to render relation attributes included.
137
+ # Following the URL rules of JSON API:
138
+ # http://jsonapi.org/format/#fetching-includes
139
+ #
140
+ # Params:
141
+ #
142
+ # +resource+::
143
+ # +Symbol+ name of resource to receive the inclusion
144
+ def inclusion_for(resource, root: false, default_includes: [])
145
+ @inclusion ||= Hashie::Mash.new
146
+ @meta ||= Hashie::Mash.new
147
+
148
+ # put the includes in place
149
+ if params[:include].is_a? Hash
150
+ @inclusion[resource] = params[:include][resource] || params[:include][resource]
151
+ elsif root
152
+ @inclusion[resource] = params[:include]
153
+ end
154
+
155
+ # splits the string into array of symbles
156
+ @inclusion[resource] = @inclusion[resource] ? @inclusion[resource].split(',').map(&:to_sym) : default_includes
157
+ end
158
+
159
+ # View Helper to set the inclusion and default_inclusion.
160
+ def set_inclusion(resource, default_includes: [])
161
+ @inclusion ||= {}
162
+ @inclusion_field ||= {}
163
+ @inclusion[resource] = default_includes if @inclusion[resource].blank?
164
+ end
165
+
166
+ # View Helper to set the inclusion details.
167
+ def set_inclusion_field(self_resource, field, id_field, class_name: nil, url: nil)
168
+ return if (@fieldset.present? && @fieldset[self_resource].present? && !@fieldset[self_resource].include?(field))
169
+
170
+ @inclusion_field ||= {}
171
+ @inclusion_field[self_resource] ||= []
172
+ field_data = {
173
+ field: field,
174
+ id_field: id_field,
175
+ class_name: class_name,
176
+ url: url
177
+ }
178
+ @inclusion_field[self_resource] << field_data
179
+ @fieldset[self_resource].delete(field) if @fieldset[self_resource].present?
180
+ end
181
+
182
+ # Getter for the inclusion data.
183
+ def inclusion(resource = nil, field = nil)
184
+ if resource.blank?
185
+ @inclusion ||= {}
186
+ elsif field.blank?
187
+ (@inclusion ||= {})[resource] ||= []
188
+ else
189
+ return false if (try(:fieldset, resource).present? && !fieldset(resource, field))
190
+ inclusion(resource).include?(field)
191
+ end
192
+ end
193
+
194
+ # Return the 'include' param description
195
+ def self.include_param_desc(example: nil, default: nil)
196
+ if default.present?
197
+ desc = "Returning compound documents that include specific associated objects, defaults to '#{default}'."
198
+ else
199
+ desc = "Returning compound documents that include specific associated objects."
200
+ end
201
+ if example.present?
202
+ "#{desc} Example value: '#{example}'"
203
+ else
204
+ desc
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,82 @@
1
+ require 'active_support'
2
+
3
+ # = Helper To Make Resource APIs Multigettable
4
+ #
5
+ # A normal resource API can let clients retrieve one specified data at a time:
6
+ #
7
+ # GET /posts/3
8
+ #
9
+ # If it's declared to be multigettable, then clients can retrieve multiple
10
+ # specified data like this:
11
+ #
12
+ # GET /posts/3,4,8,9
13
+ #
14
+ # == Usage
15
+ #
16
+ # Include this +Concern+ in your Action Controller:
17
+ #
18
+ # SamplesController < ApplicationController
19
+ # include APIHelper::Multigettable
20
+ # end
21
+ #
22
+ # or in your Grape API class:
23
+ #
24
+ # class SampleAPI < Grape::API
25
+ # include APIHelper::Multigettable
26
+ # end
27
+ #
28
+ # then use the +multiget+ method like this:
29
+ #
30
+ # resources :posts do
31
+ # # ...
32
+ # get :id do
33
+ # @post = multiget(Post, find_by: :id, max: 12)
34
+ # # ...
35
+ # end
36
+ # end
37
+ #
38
+ # <em>The +multiget+ method returns a array of or a single model, based
39
+ # directly from the requested URL.</em>
40
+ #
41
+ # There is also another helper method to determine whether the request is
42
+ # multigeting or not:
43
+ #
44
+ # multiget?(find_by: id) #=> true of false
45
+ #
46
+ # It can be used to interact with other condition and functionalities,
47
+ # like this:
48
+ #
49
+ # inclusion_for :post, root: true,
50
+ # default_includes: (multiget?(find_by: :id) ? [] : [:author])
51
+ module APIHelper::Multigettable
52
+ extend ActiveSupport::Concern
53
+
54
+ # Get multiple resources from a resource URL by specifing ids split by ','
55
+ #
56
+ # Params:
57
+ #
58
+ # +resource+::
59
+ # +ActiveRecord::Base+ or +ActiveRecord::Relation+ resource collection
60
+ # to find data from
61
+ #
62
+ # +find_by+::
63
+ # +Symbol+ the attribute that is used to find data
64
+ #
65
+ # +max+::
66
+ # +Integer+ maxium count of returning results
67
+ def multiget(resource, find_by: :id, max: 10)
68
+ ids = params[find_by].split(',')
69
+ ids = ids[0..(max - 1)]
70
+
71
+ if ids.count > 1
72
+ resource.where(find_by => ids)
73
+ else
74
+ resource.find_by(find_by => ids[0])
75
+ end
76
+ end
77
+
78
+ # Is the a multiget request?
79
+ def multiget?(find_by: :id)
80
+ params[find_by].include?(',')
81
+ end
82
+ end
@@ -0,0 +1,140 @@
1
+ require 'active_support'
2
+
3
+ # = Helper To Make Resource APIs Paginatable
4
+ #
5
+ # Paginating the requested items can avoid returning too much information
6
+ # in a single response. API callers can iterate over the results using
7
+ # pagination instead of rerteving all the data in one time, ruining the
8
+ # database connection or network.
9
+ #
10
+ # There are two parameters clients can use: +per_page+ and +page+. The former
11
+ # is used for setting how many data 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
+ # <em>The server will respond 10 items at a time.</em>
17
+ #
18
+ # Use the +page+ parameter to specify which to retrieve:
19
+ #
20
+ # GET /posts?page=5
21
+ #
22
+ # Pagination info will be provided in the HTTP Link header like this:
23
+ #
24
+ # Link: <http://api-server.dev/movies?page=1>; rel="first",
25
+ # <http://api-server.dev/movies?page=4>; rel="prev"
26
+ # <http://api-server.dev/movies?page=6>; rel="next",
27
+ # <http://api-server.dev/movies?page=238>; rel="last"
28
+ #
29
+ # <em>Line breaks are added for readability.</em>
30
+ #
31
+ # Which follows the proposed RFC 5988 standard.
32
+ #
33
+ # == Usage
34
+ #
35
+ # Include this +Concern+ in your Action Controller:
36
+ #
37
+ # SamplesController < ApplicationController
38
+ # include APIHelper::Paginatable
39
+ # end
40
+ #
41
+ # or in your Grape API class:
42
+ #
43
+ # class SampleAPI < Grape::API
44
+ # include APIHelper::Paginatable
45
+ # end
46
+ #
47
+ # then set the options for pagination in the grape method:
48
+ #
49
+ # resources :posts do
50
+ # get do
51
+ # pagination User.count, default_per_page: 25, maxium_per_page: 100
52
+ #
53
+ # # ...
54
+ # end
55
+ # end
56
+ #
57
+ # Then use the helper methods, like this:
58
+ #
59
+ # User.page(page).per(per_page)
60
+ #
61
+ # HTTP Link header will be automatically set.
62
+ module APIHelper::Paginatable
63
+ extend ActiveSupport::Concern
64
+
65
+ def pagination(items_count, default_per_page: 20, maxium_per_page: 100, set_header: true)
66
+ items_count = items_count.count if items_count.respond_to? :count
67
+
68
+ @per_page = (params[:per_page] || default_per_page).to_i
69
+ @per_page = maxium_per_page if @per_page > maxium_per_page
70
+ @per_page = 1 if @per_page < 1
71
+
72
+ items_count = 0 if items_count < 0
73
+ pages_count = (items_count.to_f / @per_page).ceil
74
+ pages_count = 1 if pages_count < 1
75
+
76
+ @page = (params[:page] || 1).to_i
77
+ @page = pages_count if @page > pages_count
78
+ @page = 1 if @page < 1
79
+
80
+ link_headers ||= []
81
+
82
+ if current_page < pages_count
83
+ link_headers << "<#{add_or_replace_uri_param(request.url, :page, current_page + 1)}>; rel=\"next\""
84
+ link_headers << "<#{add_or_replace_uri_param(request.url, :page, pages_count)}>; rel=\"last\""
85
+ end
86
+ if current_page > 1
87
+ link_headers << "<#{add_or_replace_uri_param(request.url, :page, (current_page > pages_count ? pages_count : current_page - 1))}>; rel=\"prev\""
88
+ link_headers << "<#{add_or_replace_uri_param(request.url, :page, 1)}>; rel=\"first\""
89
+ end
90
+
91
+ link_header = link_headers.join(', ')
92
+
93
+ if set_header
94
+ if self.respond_to?(:header)
95
+ self.header('Link', link_header)
96
+ self.header('X-Items-Count', items_count.to_s)
97
+ end
98
+
99
+ if defined?(response) && response.respond_to?(:headers)
100
+ response.headers['Link'] = link_header
101
+ response.headers['X-Items-Count'] = items_count.to_s
102
+ end
103
+ end
104
+
105
+ link_header
106
+ end
107
+
108
+ # Getter for the current page
109
+ def page
110
+ @page
111
+ end
112
+
113
+ alias_method :current_page, :page
114
+
115
+ # Getter for per_page
116
+ def per_page
117
+ @per_page
118
+ end
119
+
120
+ alias_method :page_with, :per_page
121
+
122
+ def add_or_replace_uri_param(url, param_name, param_value)
123
+ uri = URI(url)
124
+ params = URI.decode_www_form(uri.query || '')
125
+ params.delete_if { |param| param[0].to_s == param_name.to_s }
126
+ params << [param_name, param_value]
127
+ uri.query = URI.encode_www_form(params)
128
+ uri.to_s
129
+ end
130
+
131
+ # Return the 'per_page' param description
132
+ def self.per_page_param_desc
133
+ "Specify how many items you want each page to return."
134
+ end
135
+
136
+ # Return the 'page' param description
137
+ def self.page_param_desc
138
+ "Specify which page you want to get."
139
+ end
140
+ end
@@ -0,0 +1,89 @@
1
+ require 'active_support'
2
+
3
+ # = Helper To Make Resource APIs 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
+ # include 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
+ # # ...
34
+ # @posts = Post.order(sort)#...
35
+ # # ...
36
+ # end
37
+ # end
38
+ module APIHelper::Sortable
39
+ extend ActiveSupport::Concern
40
+
41
+ # Gets the `sort` parameter with the format 'resourses?sort=-created_at,name',
42
+ # verify and converts it into an safe Hash that can be passed into the .order
43
+ # method.
44
+ #
45
+ # Params:
46
+ #
47
+ # +default_order+::
48
+ # +Hash+ the default value to return if the sort parameter is not provided
49
+ def sortable(default_order: {})
50
+ # get the parameter
51
+ sort_by = params[:sort] || params[:sort_by]
52
+
53
+ if sort_by.is_a? String
54
+ # split it
55
+ sort_by_attrs = sort_by.gsub(/[^a-zA-Z0-9\-_,]/, '').split(',')
56
+
57
+ # save it
58
+ @sort = {}
59
+ sort_by_attrs.each do |attrb|
60
+ if attrb.match(/^-/)
61
+ @sort[attrb.gsub(/^-/, '')] = :desc
62
+ else
63
+ @sort[attrb] = :asc
64
+ end
65
+ end
66
+ else
67
+ @sort = default_order
68
+ end
69
+ end
70
+
71
+ # Helper to get the sort data
72
+ def sort
73
+ @sort
74
+ end
75
+
76
+ # Return the 'sort' param description
77
+ def self.sort_param_desc(example: nil, default: nil)
78
+ if default.present?
79
+ desc = "Specify how the returning data should be sorted, defaults to '#{default}'."
80
+ else
81
+ desc = "Specify how the returning data should be sorted."
82
+ end
83
+ if example.present?
84
+ "#{desc} Example value: '#{example}'"
85
+ else
86
+ desc
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,3 @@
1
+ module APIHelper
2
+ VERSION = "0.0.1"
3
+ end
data/lib/api_helper.rb ADDED
@@ -0,0 +1,10 @@
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
metadata ADDED
@@ -0,0 +1,116 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: api_helper
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Neson
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2015-06-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: activesupport
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '3'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '3'
69
+ description: Helpers for creating standard web API for Rails or Grape with ActiveRecord.
70
+ email:
71
+ - neson@dex.tw
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".gitignore"
77
+ - ".rspec"
78
+ - ".travis.yml"
79
+ - Gemfile
80
+ - README.md
81
+ - Rakefile
82
+ - api_helper.gemspec
83
+ - bin/console
84
+ - bin/setup
85
+ - lib/api_helper.rb
86
+ - lib/api_helper/fieldsettable.rb
87
+ - lib/api_helper/filterable.rb
88
+ - lib/api_helper/includable.rb
89
+ - lib/api_helper/multigettable.rb
90
+ - lib/api_helper/paginatable.rb
91
+ - lib/api_helper/sortable.rb
92
+ - lib/api_helper/version.rb
93
+ homepage: https://github.com/Neson/APIHelper
94
+ licenses: []
95
+ metadata: {}
96
+ post_install_message:
97
+ rdoc_options: []
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ requirements: []
111
+ rubyforge_project:
112
+ rubygems_version: 2.4.6
113
+ signing_key:
114
+ specification_version: 4
115
+ summary: Helpers for creating standard web API.
116
+ test_files: []