api_me 0.3.1 → 0.3.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|