api_helper 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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: []