skinny_controllers 0.1 → 0.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: 22e75d7a70d070311ace3af0a33cfa1df96dce4d
4
- data.tar.gz: cee31b4824b93e8d425746a443212c1f36ca7f31
3
+ metadata.gz: ad259a752520b2a0fbfd467b7f4dd50ee97ba1a2
4
+ data.tar.gz: b2a7c7b3d82895b1e66b1677f0137c6d91947503
5
5
  SHA512:
6
- metadata.gz: 0e1c99dd3bfd118eac0cb5081327328bf0dfbee23a5f93cee0a2d9fdf9f30d22eebab32aa0e7b6329a903ba88788a3e488893f12a6c0311f6153bc3a837adb61
7
- data.tar.gz: e8ad57f6833d21b4148f8f5307e262d072698f83d114a027813297e7ad831df38afcce29436964077814935c5c3fc590855c4b2ca8335da1643cdd15800873e7
6
+ metadata.gz: 5d276829328864ccc91cda85e6b7c866a7e22b397fe598645eb0b5b2e0414c9bf23b54b13233951f56e156221a351d1ecd753be7339cd28ba1e016f6d8696fee
7
+ data.tar.gz: cc2d9d76498663038ea08a401cff03672da41115cfa1aa35b9f4ac4530129387225d699351e91f46d5b686c3411e321653f7c2b1e755fa79ffc5fdf717f59e9a
data/README.md CHANGED
@@ -9,6 +9,8 @@ An implementation of role-based policies and operations to help controllers lose
9
9
 
10
10
  The goal of this project is to help API apps be more slim, and separate logic as much as possible.
11
11
 
12
+ This gem is inspired by [trailblazer](https://github.com/apotonick/trailblazer), following similar patterns, yet allowing the structure of the rails app to not be entirely overhauled.
13
+
12
14
  # Installation
13
15
 
14
16
  ```ruby
@@ -20,4 +22,117 @@ or
20
22
 
21
23
  # Usage
22
24
 
23
- TODO: this section
25
+ ## In a controller:
26
+
27
+ ```ruby
28
+ include SkinnyControllers::Diet
29
+ # ...
30
+ # in your action
31
+ render json: model
32
+ ```
33
+
34
+ and that's it!
35
+
36
+ The above does a multitude of assumptions to make sure that you can type the least amount code possible.
37
+
38
+ 1. Your controller name is based off your model name (configurable per controller)
39
+ 2. Any defined policies or operations follow the formats (though they don't have to exist):
40
+ - `#{Model.name}Policy`
41
+ - `#{Model.name}Operations`
42
+ 3. Your model responds to `find`, and `where`
43
+ 4. Your model responds to `is_accessible_to?`. This can be changed at `SkinnyControllers.accessible_to_method`
44
+
45
+ ### Your model name is different from your resource name
46
+ Lets say you have a JSON API resource that you'd like to render that has some additional/subset of data.
47
+ Maybe the model is an `Event`, and the resource an `EventSummary` (which could do some aggregation of `Event` data).
48
+
49
+ The naming of all the objects should be as follows:
50
+ - `EventSummariesController`
51
+ - `EventSummaryOperations::*`
52
+ - `EventSummaryPolicy`
53
+ - and the model is still `Event`
54
+
55
+ In `EventSummariesController`, you would make the following additions:
56
+ ```ruby
57
+ class EventSummariesController < ApiController # or whatever your superclass is
58
+ include SkinnyControllers::Diet
59
+ model_class = Event
60
+
61
+ def index
62
+ render json: model, each_serializer: EventSummariesSerializer
63
+ end
64
+
65
+ def show
66
+ render json: model, serializer: EventSummariesSerializer
67
+ end
68
+ end
69
+ ```
70
+ Note that `each_serializer` and `serializer` is not part of `SkinnyControllers`, and is part of [ActiveModel::Serializers](https://github.com/rails-api/active_model_serializers).
71
+
72
+ ## Defining Operations
73
+
74
+ Operations should be placed in `app/operations` of your rails app.
75
+
76
+ For operations concerning an `Event`, they should be under `app/operations/event_operations/`.
77
+
78
+ Using the example from the specs:
79
+ ```ruby
80
+ module EventOperations
81
+ class Read < SkinnyControllers::Operation::Base
82
+ def run
83
+ model if allowed?
84
+ end
85
+ end
86
+ end
87
+ ```
88
+
89
+ alternatively, all operation verbs can be stored in the same file under (for example) `app/operations/user_operations.rb`
90
+
91
+ ```ruby
92
+ module UserOperations
93
+ class Read < SkinnyControllers::Operation::Base
94
+ def run
95
+ model if allowed?
96
+ end
97
+ end
98
+
99
+ class ReadAll < SkinnyControllers::Operation::Base
100
+ def run
101
+ model if allowed?
102
+ end
103
+ end
104
+ end
105
+ ```
106
+
107
+
108
+ ## Defining Policies
109
+
110
+ Policies should be placed in `app/policies` of your rails app.
111
+ These are where you define your access logic, and how to decide if a user has access to the `object`
112
+
113
+ ```ruby
114
+ class EventPolicy < SkinnyControllers::Policy::Base
115
+ def read?(o = object)
116
+ o.is_accessible_to?(user)
117
+ end
118
+ end
119
+ ```
120
+
121
+
122
+ ## Globally Configurable Options
123
+
124
+ All of these can be set on `SkinnyControllers`,
125
+ e.g.:
126
+ ```ruby
127
+ SkinnyControllers.controller_namespace = 'API'
128
+ ```
129
+
130
+ The following options are available:
131
+ - `operations_namespace`
132
+ - `operations_suffix`
133
+ - `policy_suffix`
134
+ - `controller_namespace` defaults to `''`
135
+ - `allow_by_default` defaults to `true`
136
+ - `accessible_to_method` defaults to `is_accessible_to?`
137
+ - `accessible_to_scope` defaults to `accessible_to`
138
+ - `action_map` see [skinny_controllers.rb](./lib/skinny_controllers.rb#L61)
@@ -31,9 +31,11 @@ module SkinnyControllers
31
31
  end
32
32
 
33
33
  def default_operation_namespace_for(model_name)
34
- parent_namespace = Operation::Default.name.deconstantize
35
- namespace = "#{parent_namespace}::#{model_name}".safe_constantize
36
- namespace || Operation.const_set(model_name, Module.new)
34
+ desired_namespace = operation_namespace_from_model(model_name)
35
+
36
+ parent_namespace = SkinnyControllers.operations_namespace
37
+ namespace = "#{parent_namespace}::#{desired_namespace}".safe_constantize
38
+ namespace || Object.const_set(desired_namespace, Module.new)
37
39
  end
38
40
 
39
41
  # abstraction for `operation.run`
@@ -46,6 +48,10 @@ module SkinnyControllers
46
48
 
47
49
  private
48
50
 
51
+ def operation_namespace_from_model(model_name)
52
+ "#{model_name}#{SkinnyControllers::operations_suffix}"
53
+ end
54
+
49
55
  # action name is inherited from ActionController::Base
50
56
  # http://www.rubydoc.info/docs/rails/2.3.8/ActionController%2FBase%3Aaction_name
51
57
  def verb_for_action
@@ -65,8 +71,9 @@ module SkinnyControllers
65
71
  end
66
72
 
67
73
  def operation_class_from_model(model_name)
68
- prefix = SkinnyControllers.operation_namespace
69
- "#{prefix}::#{model_name}::#{verb_for_action}"
74
+ prefix = SkinnyControllers.operations_namespace
75
+ namespace = operation_namespace_from_model(model_name)
76
+ "#{prefix}::#{namespace}::#{verb_for_action}"
70
77
  end
71
78
 
72
79
  def controller_name_prefix
@@ -18,10 +18,9 @@ module SkinnyControllers
18
18
 
19
19
  attr_accessor :params, :current_user, :authorized_via_parent
20
20
 
21
- class << self
22
- def run(current_user, params)
23
- new(current_user, params).run
24
- end
21
+ def self.run(current_user, params)
22
+ object = self.new(current_user, params)
23
+ object.run
25
24
  end
26
25
 
27
26
  def initialize(current_user, params)
@@ -42,11 +41,29 @@ module SkinnyControllers
42
41
  end
43
42
 
44
43
  def object_class
45
- @object_class ||= object_type_of_interest.demodulize.constantize
44
+ unless @object_class
45
+ # "Namespace::Model" => "Model"
46
+ model_name = object_type_of_interest.demodulize
47
+ # "Model" => Model
48
+ @object_class = model_name.constantize
49
+ end
50
+
51
+ @object_class
46
52
  end
47
53
 
48
54
  def object_type_of_interest
49
- @object_type_name ||= self.class.name.deconstantize.demodulize
55
+ unless @object_type_name
56
+ # Namespace::ModelOperations::Verb
57
+ klass_name = self.class.name
58
+ # Namespace::ModelOperations::Verb => Namespace::ModelOperations
59
+ namespace = klass_name.deconstantize
60
+ # Namespace::ModelOperations => ModelOperations
61
+ nested_namespace = namespace.demodulize
62
+ # ModelOperations => Model
63
+ @object_type_name = nested_namespace.gsub(SkinnyControllers.operations_suffix, '')
64
+ end
65
+
66
+ @object_type_name
50
67
  end
51
68
 
52
69
  def association_name_from_object
@@ -4,12 +4,14 @@ module SkinnyControllers
4
4
  def model
5
5
  # TODO: not sure if multiple ids is a good idea here
6
6
  # if we don't have a(ny) id(s), get all of them
7
+
8
+
7
9
  @model ||=
8
10
  if id_from_params
9
11
  model_from_id
10
12
  elsif params[:scope]
11
13
  model_from_scope
12
- elsif key = params.keys.grep(/_id$/)
14
+ elsif (key = params.keys.grep(/\_id$/)).present?
13
15
  # hopefully there is only ever one of these passed
14
16
  model_from_named_id(key.first)
15
17
  else
@@ -17,10 +19,15 @@ module SkinnyControllers
17
19
  end
18
20
  end
19
21
 
22
+ def sanitized_params
23
+ keys = (object_class.column_names & params.keys)
24
+ params.slice(*keys).symbolize_keys
25
+ end
26
+
20
27
  def scoped_model(scoped_params)
21
28
  unless @scoped_model
22
29
  klass_name = scoped_params[:type]
23
- operation_name = "Operations::#{klass_name}::Read"
30
+ operation_name = operation_for(klass_name, 'Read'.freeze)
24
31
  operation = operation_name.constantize.new(current_user, id: scoped_params[:id])
25
32
  @scoped_model = operation.run
26
33
  self.authorized_via_parent = !!@scoped_model
@@ -29,8 +36,26 @@ module SkinnyControllers
29
36
  @scoped_model
30
37
  end
31
38
 
39
+ def operation_for(klass_name, verb)
40
+ operations_class_namespace +
41
+ klass_name +
42
+ SkinnyControllers.operations_suffix +
43
+ "::#{verb}"
44
+ end
45
+
46
+ def operations_class_namespace
47
+ namespace = SkinnyControllers.policies_namespace
48
+ "#{namespace}::" if namespace
49
+ end
50
+
32
51
  def model_from_params
33
- object_class.where(params).accessible_to(current_user)
52
+ ar_proxy = object_class.where(sanitized_params)
53
+
54
+ if ar_proxy.respond_to? SkinnyControllers.accessible_to_scope
55
+ return ar_proxy.accessible_to(current_user)
56
+ end
57
+
58
+ ar_proxy
34
59
  end
35
60
 
36
61
  def model_from_named_id(key)
@@ -1,22 +1,27 @@
1
1
  module SkinnyControllers
2
2
  module Operation
3
3
  module PolicyHelpers
4
- POLICY_CLASS_PREFIX = 'Policy::'.freeze
5
- POLICY_CLASS_SUFFIX = 'Policy'.freeze
6
- POLICY_SUFFIX = '?'.freeze
4
+ POLICY_METHOD_SUFFIX = '?'.freeze
7
5
 
6
+ # Takes the class name of self and converts it to a Policy class name
7
+ #
8
+ # @example In Operation::Event::Read, Policy::EventPolicy is returned
8
9
  def policy_class
9
10
  @policy_class ||= (
10
- POLICY_CLASS_PREFIX +
11
+ policy_class_namespace +
11
12
  object_type_of_interest +
12
- POLICY_CLASS_SUFFIX
13
+ SkinnyControllers.policy_suffix
13
14
  ).constantize
14
15
  end
15
16
 
16
- def policy_name
17
- @policy_name ||= self.class.name.demodulize.downcase + POLICY_SUFFIX
17
+ # Converts the class name to the method name to call on the policy
18
+ #
19
+ # @example Operation::Event::Read would become read?
20
+ def policy_method_name
21
+ @policy_method_name ||= self.class.name.demodulize.downcase + POLICY_METHOD_SUFFIX
18
22
  end
19
23
 
24
+ # @return a new policy object and caches it
20
25
  def policy_for(object)
21
26
  @policy ||= policy_class.new(
22
27
  current_user,
@@ -25,12 +30,19 @@ module SkinnyControllers
25
30
  end
26
31
 
27
32
  def allowed?
28
- policy_for(model)
33
+ allowed_for?(model)
29
34
  end
30
35
 
31
36
  # checks the policy
32
37
  def allowed_for?(object)
33
- policy_for(object).send(policy_name)
38
+ policy_for(object).send(policy_method_name)
39
+ end
40
+
41
+ private
42
+
43
+ def policy_class_namespace
44
+ namespace = SkinnyControllers.policies_namespace
45
+ "#{namespace}::" if namespace
34
46
  end
35
47
  end
36
48
  end
@@ -3,21 +3,40 @@ module SkinnyControllers
3
3
  class Base
4
4
  attr_accessor :user, :object, :authorized_via_parent
5
5
 
6
+ # @param [User] user the being to check if they have access to `object`
7
+ # @param [ActiveRecord::Base] object the object that we are wanting to check
8
+ # the authorization of `user` for
9
+ # @param [Boolean] authorized_via_parent if this object is authorized via
10
+ # a prior authorization from a parent class / association
6
11
  def initialize(user, object, authorized_via_parent: false)
7
12
  self.user = user
8
13
  self.object = object
9
14
  self.authorized_via_parent = authorized_via_parent
10
15
  end
11
16
 
17
+ # if a method is not defined for a particular verb or action,
18
+ # this will be used.
19
+ #
20
+ # @example Operation::Object::SendReceipt.run is called, since
21
+ # `send_receipt` doesn't exist in this class, this `default?`
22
+ # method will be used.
12
23
  def default?
13
24
  SkinnyControllers.allow_by_default
14
25
  end
15
26
 
16
- # defaults
27
+ # this should be used when checking access to a single object
17
28
  def read?(o = object)
18
- o.is_accessible_to? user
29
+ o.send(accessible_method, user)
19
30
  end
20
31
 
32
+ # this should be used when checking access to multilpe objects
33
+ # it will call `read?` on each object of the array
34
+ #
35
+ # if the operation used a scope to find records from
36
+ # an association, then `authorized_via_parent` could be true,
37
+ # in which case, the loop would be skipped.
38
+ #
39
+ # TODO: think of a way to override the authorized_via_parent functionality
21
40
  def read_all?
22
41
  return true if authorized_via_parent
23
42
  # This is expensive, so try to avoid it
@@ -27,6 +46,12 @@ module SkinnyControllers
27
46
  accessible = object.map { |ea| read?(ea) }
28
47
  accessible.all?
29
48
  end
49
+
50
+ private
51
+
52
+ def accessible_method
53
+ SkinnyControllers.accessible_to_method
54
+ end
30
55
  end
31
56
  end
32
57
  end
@@ -1,3 +1,3 @@
1
1
  module SkinnyControllers
2
- VERSION = 0.1
2
+ VERSION = 0.2
3
3
  end
@@ -15,6 +15,12 @@ require 'skinny_controllers/operation/default'
15
15
  require 'skinny_controllers/diet'
16
16
  require 'skinny_controllers/version'
17
17
 
18
+ # load the policies and operations to the top level name space.
19
+ if defined? Rails
20
+ $LOAD_PATH.unshift Rails.root + '/app/operations'
21
+ $LOAD_PATH.unshift Rails.root + '/app/policies'
22
+ end
23
+
18
24
  module SkinnyControllers
19
25
  # Tells the Diet what namespace of the controller
20
26
  # isn't going to be part of the model name
@@ -25,15 +31,34 @@ module SkinnyControllers
25
31
  # # 'API::' would be removed from 'API::Namespace::ObjectNamesController'
26
32
  cattr_accessor :controller_namespace
27
33
 
28
- #
29
- cattr_accessor :operation_namespace do
30
- 'Operation'.freeze
34
+ cattr_accessor :operations_suffix do
35
+ 'Operations'
36
+ end
37
+
38
+ cattr_accessor :policy_suffix do
39
+ 'Policy'
40
+ end
41
+
42
+ cattr_accessor :operations_namespace do
43
+ ''.freeze
44
+ end
45
+
46
+ cattr_accessor :policies_namespace do
47
+ ''.freeze
31
48
  end
32
49
 
33
50
  cattr_accessor :allow_by_default do
34
51
  true
35
52
  end
36
53
 
54
+ cattr_accessor :accessible_to_method do
55
+ :is_accessible_to?
56
+ end
57
+
58
+ cattr_accessor :accessible_to_scope do
59
+ :accessible_to
60
+ end
61
+
37
62
  # the diet uses ActionController::Base's
38
63
  # `action_name` method to get the current action.
39
64
  # From that action, we map what verb we want to use for our operation
@@ -45,9 +70,11 @@ module SkinnyControllers
45
70
  cattr_accessor :action_map do
46
71
  {
47
72
  'default'.freeze => DefaultVerbs::Read,
48
- 'create'.freeze => DefaultVerbs::Create,
49
73
  'index'.freeze => DefaultVerbs::ReadAll,
50
74
  'destroy'.freeze => DefaultVerbs::Delete,
75
+ # these two are redundant, as the action will be
76
+ # converted to a verb via inflection
77
+ 'create'.freeze => DefaultVerbs::Create,
51
78
  'update'.freeze => DefaultVerbs::Update
52
79
  }
53
80
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: skinny_controllers
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.1'
4
+ version: '0.2'
5
5
  platform: ruby
6
6
  authors:
7
7
  - L. Preston Sego III
@@ -25,7 +25,7 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
- name: rails
28
+ name: rubocop
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
@@ -39,7 +39,7 @@ dependencies:
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
- name: awesome_print
42
+ name: rspec
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - ">="
@@ -53,7 +53,21 @@ dependencies:
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: rspec
56
+ name: codeclimate-test-reporter
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: awesome_print
57
71
  requirement: !ruby/object:Gem::Requirement
58
72
  requirements:
59
73
  - - ">="
@@ -81,7 +95,7 @@ dependencies:
81
95
  - !ruby/object:Gem::Version
82
96
  version: '0'
83
97
  - !ruby/object:Gem::Dependency
84
- name: codeclimate-test-reporter
98
+ name: bundler
85
99
  requirement: !ruby/object:Gem::Requirement
86
100
  requirements:
87
101
  - - ">="
@@ -95,7 +109,63 @@ dependencies:
95
109
  - !ruby/object:Gem::Version
96
110
  version: '0'
97
111
  - !ruby/object:Gem::Dependency
98
- name: rubocop
112
+ name: rails
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: factory_girl
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: factory_girl_rails
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: rspec-rails
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: sqlite3
99
169
  requirement: !ruby/object:Gem::Requirement
100
170
  requirements:
101
171
  - - ">="
@@ -148,5 +218,5 @@ rubyforge_project:
148
218
  rubygems_version: 2.4.8
149
219
  signing_key:
150
220
  specification_version: 4
151
- summary: SkinnyControllers-0.1
221
+ summary: SkinnyControllers-0.2
152
222
  test_files: []