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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6b1bab29cbafd8eaab2edc45f10f6e1875a3b49d
4
- data.tar.gz: 5f542f1dac50766aec5be47c56fb9a819ed11c7d
3
+ metadata.gz: 92daadad80db70e8aef932f76f51c4588438af3f
4
+ data.tar.gz: 7a206ff30375d224b053b2c522fe224d130be1f1
5
5
  SHA512:
6
- metadata.gz: bef770b426760b1c06de0d917ada87a8946be652dedcba3cbdf3d3e13ceb721110fdb17704df8cfc28700021f6fcb3a67bb074fa9e30165e8344ee3f986708d4
7
- data.tar.gz: 1134ce12dc9dee16f652d929b76a69dc98f39332efd6d71066b9dc9c48d0c98c1a0692f839519a12ef79fc20615af326be09e4f2d5847d6967f3859bfcac0de6
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
- Or
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
- #### This gem uses the following libraries:
30
- * Pundit
31
- * Active Model Serializers (0.8)
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 params_klass_symbol
54
- model_klass.name.demodulize.underscore.to_sym
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
- render json: @scoped_objects, each_serializer: serializer_klass
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
@@ -0,0 +1,7 @@
1
+ module ApiMe
2
+ class BaseFilter
3
+ include SearchObject.module
4
+
5
+ option(:ids) { |scope, value| scope.where("id" => value) }
6
+ end
7
+ end
@@ -1,3 +1,3 @@
1
1
  module ApiMe
2
- VERSION = '0.3.1'
2
+ VERSION = '0.3.2'
3
3
  end
@@ -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 api_controller foo_bar foo:references{polymorphic} bar:belongs_to name description
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 -%>
@@ -2,7 +2,7 @@ Description:
2
2
  Explain the generator
3
3
 
4
4
  Example:
5
- rails generate api_policy Thing
5
+ rails generate api_me:policy Thing
6
6
 
7
7
  This will create:
8
8
  what/will/it/create
@@ -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
@@ -0,0 +1,3 @@
1
+ class Api::V1::PostsController < ApplicationController
2
+ include ApiMe
3
+ end
@@ -0,0 +1,7 @@
1
+ require 'search_object'
2
+
3
+ class UserFilter < ApiMe::BaseFilter
4
+ include ::SearchObject.module
5
+
6
+ option(:search) { |scope, value| scope.where("username LIKE ?", "%#{value}%") }
7
+ end
@@ -0,0 +1,2 @@
1
+ class Post < ActiveRecord::Base
2
+ end
@@ -0,0 +1,4 @@
1
+ class PostPolicy < ApplicationPolicy
2
+ class Scope < ApplicationPolicy::Scope
3
+ end
4
+ end
@@ -0,0 +1,3 @@
1
+ class PostSerializer < ActiveModel::Serializer
2
+ attributes :name
3
+ end
@@ -2,6 +2,7 @@ Rails.application.routes.draw do
2
2
  namespace :api do
3
3
  namespace :v1 do
4
4
  resources :users
5
+ resources :posts
5
6
  resources :fails
6
7
  resources :multi_word_resources
7
8
  end
@@ -3,4 +3,9 @@ ActiveRecord::Schema.define do
3
3
  t.string :username
4
4
  t.timestamps
5
5
  end
6
+
7
+ create_table :posts, force: true do |t|
8
+ t.string :name
9
+ t.timestamps
10
+ end
6
11
  end
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.1
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-07 00:00:00.000000000 Z
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