api_me 0.3.1 → 0.3.2
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 +4 -4
- data/README.md +72 -7
- data/api_me.gemspec +1 -0
- data/lib/api_me.rb +31 -11
- data/lib/api_me/base_filter.rb +7 -0
- data/lib/api_me/version.rb +1 -1
- data/lib/generators/api_me/controller/USAGE +1 -1
- data/lib/generators/api_me/filter/USAGE +0 -0
- data/lib/generators/api_me/filter/filter_generator.rb +58 -0
- data/lib/generators/api_me/filter/templates/filter.rb +9 -0
- data/lib/generators/api_me/policy/USAGE +1 -1
- data/spec/acceptance/api/v1/posts_spec.rb +33 -0
- data/spec/acceptance/api/v1/users_spec.rb +15 -0
- data/spec/internal/app/controllers/api/v1/posts_controller.rb +3 -0
- data/spec/internal/app/filters/user_filter.rb +7 -0
- data/spec/internal/app/models/post.rb +2 -0
- data/spec/internal/app/policies/post_policy.rb +4 -0
- data/spec/internal/app/serializers/post_serializer.rb +3 -0
- data/spec/internal/config/routes.rb +1 -0
- data/spec/internal/db/schema.rb +5 -0
- metadata +26 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 92daadad80db70e8aef932f76f51c4588438af3f
|
4
|
+
data.tar.gz: 7a206ff30375d224b053b2c522fe224d130be1f1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 900a7acabcbbc6fe0ca576c7f94cb1be4e2acfc4a63d85209dacd78603dae7273aca01e6b8ae8acccdc5a95db43ef057aaef714b651013fc9347c7df864e5a5b
|
7
|
+
data.tar.gz: 09053b626b1c8120569ba25202356db997985e1f79bedc01ae770af1b78d3e5dcc7caca4f9fac2279d14ff9a6406f92e7a5fd48330906d689e7d478bd7d23d0a
|
data/README.md
CHANGED
@@ -7,6 +7,11 @@ ApiMe
|
|
7
7
|
### A gem for building RESTful Api resources in Rails
|
8
8
|
ApiMe provides a set of generators and base classes to assist with building Restful API's in Ruby on Rails.
|
9
9
|
|
10
|
+
### Details
|
11
|
+
Api controllers use the fantastic [Pundit](https://github.com/elabs/pundit) gem for authorization and parameter whitelisting, [Active Model Serializers ver 0.8](https://github.com/rails-api/active_model_serializers/tree/0-8-stable) for resource serialization, and [SearchObject](https://github.com/RStankov/SearchObject) for list filtering. The model, filter, serializer, and policy that the controller uses by default can all be overriden, along with other optional parameters.
|
12
|
+
|
13
|
+
The primary goal of this gem was to keep things simple so that customization is fairly straight forward through the separating concerns and overrides. Reusing existing libraries was a primary goal during the design, hence the overall simplicity of this gem. We currently use this gem internally at [Inigo](inigo.io) and are committed to its ongoing maintenance.
|
14
|
+
|
10
15
|
### Usage
|
11
16
|
`rails g api_me:resource user organization:belongs_to name:string ...`
|
12
17
|
|
@@ -15,24 +20,84 @@ this generates the following:
|
|
15
20
|
* app/controllers/api/v1/users_controller.rb
|
16
21
|
* app/policies/user_policy.rb
|
17
22
|
* app/serializers/user_serializer.rb
|
18
|
-
* app/models/user.rb
|
19
23
|
|
20
|
-
|
24
|
+
and also essentially calls:
|
25
|
+
* `rails g model user organization:belongs_to name:string ...`
|
26
|
+
Which generates the model et al as specified.
|
21
27
|
|
22
|
-
users_controller.rb
|
28
|
+
users_controller.rb:
|
23
29
|
````rb
|
24
30
|
class UsersController < ApplicationController
|
25
31
|
include ApiMe
|
32
|
+
|
33
|
+
end
|
34
|
+
````
|
35
|
+
POST (create) and PUT (update) requests are expected to post parameters to the singular underscored name of the model by default (I.E. `{"user": {"name": "Test"}}` for a user model), but this can be overriden by overriding `def params_klass_symbol`, or more in-depth by overriding `def object_params`. If `def object_params` is overriden, parameters are also expected be whitelisted inside of this method.
|
36
|
+
|
37
|
+
models/user.rb:
|
38
|
+
````rb
|
39
|
+
# Standard Rails generator used
|
40
|
+
class User < ActiveRecord::Base
|
41
|
+
belongs_to :organization
|
42
|
+
end
|
43
|
+
````
|
44
|
+
|
45
|
+
policies/user_policy.rb (See [Pundit](https://github.com/elabs/pundit) for details):
|
46
|
+
````rb
|
47
|
+
class UserPolicy < ApplicationPolicy
|
48
|
+
# Authorizes what parameters will be whitelisted, see [Pundit](https://github.com/elabs/pundit) for details
|
49
|
+
def permitted_attributes
|
50
|
+
[:id, :organization_id, :name]
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
````
|
55
|
+
|
56
|
+
serializers/user_serializer.rb (See [Active Model Serializers ver 0.8](https://github.com/rails-api/active_model_serializers/tree/0-8-stable) for details):
|
57
|
+
````rb
|
58
|
+
class UserSerializer < ActiveModel::Serializer
|
59
|
+
attributes :id, :name, :organization_id
|
26
60
|
end
|
27
61
|
````
|
28
62
|
|
29
|
-
|
30
|
-
|
31
|
-
|
63
|
+
filters/user_filter.rb (See [SearchObject](https://github.com/RStankov/SearchObject) for details):
|
64
|
+
````rb
|
65
|
+
require 'search_object'
|
66
|
+
|
67
|
+
class UserFilter < ApiMe::BaseFilter
|
68
|
+
include ::SearchObject.module #required
|
69
|
+
|
70
|
+
# Add custom filter logic here
|
71
|
+
# Ex:
|
72
|
+
# option(:search) { |scope, value| scope.where("username LIKE ?", "%#{value}%") }
|
73
|
+
end
|
74
|
+
````
|
75
|
+
The ApiMe::BaseFilter is called if no filter exists for the resource, by default the base filter provides filtering by ids for convenience. I.E a GET to `/api/v1/users?ids%5B%5D=1&ids%5B%5D=3` would return users filtered by ids of 1 and 3. All other filters are expected by default to be located at `params[:filters]` and not at the base level.
|
76
|
+
|
77
|
+
### Overrides
|
78
|
+
Overriding the default model class, serializer class, filter class, and filter parameter can be done like so:
|
79
|
+
|
80
|
+
users_controller.rb:
|
81
|
+
````rb
|
82
|
+
class UsersController < ApplicationController
|
83
|
+
include ApiMe
|
84
|
+
|
85
|
+
model FakeUser
|
86
|
+
serialzier RealUserSerialzier
|
87
|
+
|
88
|
+
def filter_klass
|
89
|
+
FancyUserFilter
|
90
|
+
end
|
91
|
+
|
92
|
+
def filter_params
|
93
|
+
params[:meta][:filters]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
````
|
32
97
|
|
33
98
|
#### Todo:
|
34
|
-
- [ ] Add the ability to specify resource filters
|
35
99
|
- [ ] Add the ability to specify the api controller path (I.E. app/controllers/api/v2)
|
100
|
+
- [ ] Add the ability to inject the resource route into the routes file in the resource generators
|
36
101
|
|
37
102
|
## License
|
38
103
|
Copyright (c) 2014, Api Me is developed and maintained by Sam Clopton, and is released under the open MIT Licence.
|
data/api_me.gemspec
CHANGED
@@ -22,6 +22,7 @@ Gem::Specification.new do |s|
|
|
22
22
|
s.add_runtime_dependency 'activesupport', '>= 3.2.0'
|
23
23
|
s.add_runtime_dependency 'pundit', '~> 0.1'
|
24
24
|
s.add_runtime_dependency 'active_model_serializers', '~> 0.8.0'
|
25
|
+
s.add_runtime_dependency 'search_object', '~> 1.0'
|
25
26
|
|
26
27
|
s.add_development_dependency 'combustion', '~> 0.5.1'
|
27
28
|
s.add_development_dependency 'rspec-rails', '~> 3'
|
data/lib/api_me.rb
CHANGED
@@ -1,18 +1,16 @@
|
|
1
1
|
require 'active_support/concern'
|
2
2
|
require 'pundit'
|
3
|
+
require 'search_object'
|
3
4
|
|
4
5
|
require 'api_me/version'
|
6
|
+
require 'api_me/base_filter'
|
5
7
|
|
6
8
|
module ApiMe
|
7
9
|
extend ActiveSupport::Concern
|
8
10
|
include ::Pundit
|
9
11
|
|
10
12
|
included do
|
11
|
-
|
12
13
|
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
|
13
|
-
|
14
|
-
after_action :verify_authorized, except: :index
|
15
|
-
after_action :verify_policy_scoped, only: :index
|
16
14
|
end
|
17
15
|
|
18
16
|
module ClassMethods
|
@@ -42,6 +40,10 @@ module ApiMe
|
|
42
40
|
end
|
43
41
|
end
|
44
42
|
|
43
|
+
def filter_klass
|
44
|
+
@filter_klass ||= filter_klass_name.safe_constantize || ::ApiMe::BaseFilter
|
45
|
+
end
|
46
|
+
|
45
47
|
def model_klass_name
|
46
48
|
@model_klass_name ||= name.demodulize.sub(/Controller$/, '').singularize
|
47
49
|
end
|
@@ -50,14 +52,24 @@ module ApiMe
|
|
50
52
|
@serializer_klass_name ||= "#{name.demodulize.sub(/Controller$/, '').singularize}Serializer"
|
51
53
|
end
|
52
54
|
|
53
|
-
def
|
54
|
-
|
55
|
+
def filter_klass_name
|
56
|
+
@filter_klass_name ||= "#{name.demodulize.sub(/Controller$/, '').singularize}Filter"
|
55
57
|
end
|
56
58
|
end
|
57
59
|
|
60
|
+
# Currently merge params[:ids] in filters hash
|
61
|
+
# to support common use case of filtering ids using
|
62
|
+
# the top level ids array param. Would eventually like
|
63
|
+
# to move to support the jsonapi.org standard closer.
|
58
64
|
def index
|
65
|
+
ids_filter_hash = params[:ids] ? {ids: params[:ids]} : {}
|
59
66
|
@scoped_objects = policy_scope(model_klass.all)
|
60
|
-
|
67
|
+
@filter_objects = filter_klass.new({
|
68
|
+
scope: @scoped_objects,
|
69
|
+
filters: (filter_params || {}).merge(ids_filter_hash)
|
70
|
+
})
|
71
|
+
|
72
|
+
render json: @filter_objects.results, each_serializer: serializer_klass
|
61
73
|
end
|
62
74
|
|
63
75
|
def show
|
@@ -114,10 +126,6 @@ module ApiMe
|
|
114
126
|
render json: payload, status: 403
|
115
127
|
end
|
116
128
|
|
117
|
-
def params_klass_symbol
|
118
|
-
self.class.params_klass_symbol
|
119
|
-
end
|
120
|
-
|
121
129
|
def model_klass
|
122
130
|
self.class.model_klass
|
123
131
|
end
|
@@ -125,4 +133,16 @@ module ApiMe
|
|
125
133
|
def serializer_klass
|
126
134
|
self.class.serializer_klass
|
127
135
|
end
|
136
|
+
|
137
|
+
def filter_klass
|
138
|
+
self.class.filter_klass
|
139
|
+
end
|
140
|
+
|
141
|
+
def params_klass_symbol
|
142
|
+
model_klass.name.demodulize.underscore.to_sym
|
143
|
+
end
|
144
|
+
|
145
|
+
def filter_params
|
146
|
+
params[:filters]
|
147
|
+
end
|
128
148
|
end
|
data/lib/api_me/version.rb
CHANGED
@@ -2,7 +2,7 @@ Description:
|
|
2
2
|
This generator generates an api controller that inherits from ApiMe::BaseController
|
3
3
|
|
4
4
|
Example:
|
5
|
-
rails generate
|
5
|
+
rails generate api_me:controller foo_bar foo:references{polymorphic} bar:belongs_to name description
|
6
6
|
|
7
7
|
This will create:
|
8
8
|
app/controllers/api/v1/foo_bars_controller.rb
|
File without changes
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module ApiMe
|
2
|
+
module Generators
|
3
|
+
class FilterGenerator < ::Rails::Generators::NamedBase
|
4
|
+
source_root File.expand_path('../templates', __FILE__)
|
5
|
+
check_class_collision suffix: 'Filter'
|
6
|
+
|
7
|
+
argument :attributes, type: :array, default: [], banner: 'field field'
|
8
|
+
|
9
|
+
class_option :parent, type: :string, desc: 'The parent class for the generated filter'
|
10
|
+
|
11
|
+
def create_api_filter_file
|
12
|
+
template 'filter.rb', File.join('app/filters', "#{singular_name}_filter.rb")
|
13
|
+
end
|
14
|
+
|
15
|
+
def filter_class_name
|
16
|
+
"#{class_name}Filter"
|
17
|
+
end
|
18
|
+
|
19
|
+
def attributes_names
|
20
|
+
attributes.select { |attr| !attr.reference? }.map { |a| a.name.to_sym }
|
21
|
+
end
|
22
|
+
|
23
|
+
def associations
|
24
|
+
attributes.select(&:reference?)
|
25
|
+
end
|
26
|
+
|
27
|
+
def nonpolymorphic_attribute_names
|
28
|
+
associations.select { |attr| attr.type.in?([:belongs_to, :references]) }
|
29
|
+
.reject { |attr| attr.attr_options.fetch(:polymorphic, false) }
|
30
|
+
.map { |attr| "#{attr.name}_id".to_sym }
|
31
|
+
end
|
32
|
+
|
33
|
+
def polymorphic_attribute_names
|
34
|
+
associations.select { |attr| attr.type.in?([:belongs_to, :references]) }
|
35
|
+
.select { |attr| attr.attr_options.fetch(:polymorphic, false) }
|
36
|
+
.map { |attr| ["#{attr.name}_id".to_sym, "#{attr.name}_type".to_sym] }.flatten
|
37
|
+
end
|
38
|
+
|
39
|
+
def association_attribute_names
|
40
|
+
nonpolymorphic_attribute_names + (polymorphic_attribute_names)
|
41
|
+
end
|
42
|
+
|
43
|
+
def strong_parameters
|
44
|
+
(attributes_names + association_attribute_names).map(&:inspect).join(', ')
|
45
|
+
end
|
46
|
+
|
47
|
+
def parent_class_name
|
48
|
+
if options[:parent]
|
49
|
+
options[:parent]
|
50
|
+
else
|
51
|
+
'ApiMe::BaseFilter'
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
hook_for :test_framework
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
<% module_namespacing do -%>
|
2
|
+
class <%= filter_class_name %> < <%= parent_class_name %>
|
3
|
+
include ::SearchObject.module #required
|
4
|
+
|
5
|
+
# Add custom filter logic here
|
6
|
+
# Ex:
|
7
|
+
# option(:search) { |scope, value| scope.where("username LIKE ?", "%#{value}%") }
|
8
|
+
end
|
9
|
+
<% end -%>
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Users API' do
|
4
|
+
it 'sends the list of posts using the default filter' do
|
5
|
+
posts = [
|
6
|
+
Post.create(name: "test"),
|
7
|
+
Post.create(name: "test 2")
|
8
|
+
]
|
9
|
+
|
10
|
+
get '/api/v1/posts'
|
11
|
+
|
12
|
+
expect(last_response.status).to eq(200)
|
13
|
+
json = JSON.parse(last_response.body)
|
14
|
+
|
15
|
+
expect(json['posts'].length).to eq(2)
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'sends posts filtered by ids' do
|
19
|
+
posts = [
|
20
|
+
Post.create(name: "test"),
|
21
|
+
Post.create(name: "test 2"),
|
22
|
+
Post.create(name: "test 3")
|
23
|
+
]
|
24
|
+
|
25
|
+
get '/api/v1/posts?ids%5B%5D=' + posts[0].id.to_s +
|
26
|
+
'&ids%5B%5D=' + posts[2].id.to_s
|
27
|
+
|
28
|
+
expect(last_response.status).to eq(200)
|
29
|
+
json = JSON.parse(last_response.body)
|
30
|
+
|
31
|
+
expect(json['posts'].length).to eq(2)
|
32
|
+
end
|
33
|
+
end
|
@@ -62,4 +62,19 @@ describe 'Users API' do
|
|
62
62
|
expect(last_response.status).to eq(204)
|
63
63
|
expect(does_user_exist).to eq(false)
|
64
64
|
end
|
65
|
+
|
66
|
+
it 'sends a filtered list of users' do
|
67
|
+
users = [
|
68
|
+
User.create(username: 'Test'),
|
69
|
+
User.create(username: 'Demo'),
|
70
|
+
User.create(username: 'Test 2')
|
71
|
+
]
|
72
|
+
|
73
|
+
get '/api/v1/users?filters%5Bsearch%5D=Test'
|
74
|
+
|
75
|
+
expect(last_response.status).to eq(200)
|
76
|
+
json = JSON.parse(last_response.body)
|
77
|
+
|
78
|
+
expect(json['users'].length).to eq(2)
|
79
|
+
end
|
65
80
|
end
|
data/spec/internal/db/schema.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: api_me
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sam Clopton
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-11-
|
11
|
+
date: 2014-11-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -66,6 +66,20 @@ dependencies:
|
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: 0.8.0
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: search_object
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.0'
|
69
83
|
- !ruby/object:Gem::Dependency
|
70
84
|
name: combustion
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -154,27 +168,37 @@ files:
|
|
154
168
|
- api_me.gemspec
|
155
169
|
- config.ru
|
156
170
|
- lib/api_me.rb
|
171
|
+
- lib/api_me/base_filter.rb
|
157
172
|
- lib/api_me/version.rb
|
158
173
|
- lib/generators/api_me/controller/USAGE
|
159
174
|
- lib/generators/api_me/controller/controller_generator.rb
|
160
175
|
- lib/generators/api_me/controller/templates/controller.rb
|
176
|
+
- lib/generators/api_me/filter/USAGE
|
177
|
+
- lib/generators/api_me/filter/filter_generator.rb
|
178
|
+
- lib/generators/api_me/filter/templates/filter.rb
|
161
179
|
- lib/generators/api_me/policy/USAGE
|
162
180
|
- lib/generators/api_me/policy/policy_generator.rb
|
163
181
|
- lib/generators/api_me/policy/templates/policy.rb
|
164
182
|
- lib/generators/api_me/resource/USAGE
|
165
183
|
- lib/generators/api_me/resource/resource_generator.rb
|
166
184
|
- spec/acceptance/api/v1/fails_spec.rb
|
185
|
+
- spec/acceptance/api/v1/posts_spec.rb
|
167
186
|
- spec/acceptance/api/v1/users_spec.rb
|
168
187
|
- spec/acceptance/multi_word_resource_spec.rb
|
169
188
|
- spec/internal/app/controllers/api/v1/fails_controller.rb
|
170
189
|
- spec/internal/app/controllers/api/v1/multi_word_resources_controller.rb
|
190
|
+
- spec/internal/app/controllers/api/v1/posts_controller.rb
|
171
191
|
- spec/internal/app/controllers/api/v1/users_controller.rb
|
172
192
|
- spec/internal/app/controllers/application_controller.rb
|
193
|
+
- spec/internal/app/filters/user_filter.rb
|
194
|
+
- spec/internal/app/models/post.rb
|
173
195
|
- spec/internal/app/models/test_model.rb
|
174
196
|
- spec/internal/app/models/user.rb
|
175
197
|
- spec/internal/app/policies/application_policy.rb
|
198
|
+
- spec/internal/app/policies/post_policy.rb
|
176
199
|
- spec/internal/app/policies/test_model_policy.rb
|
177
200
|
- spec/internal/app/policies/user_policy.rb
|
201
|
+
- spec/internal/app/serializers/post_serializer.rb
|
178
202
|
- spec/internal/app/serializers/test_model_serializer.rb
|
179
203
|
- spec/internal/app/serializers/user_serializer.rb
|
180
204
|
- spec/internal/config/database.yml
|