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 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