skinny_controllers 0.1 → 0.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: 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: []