skinny_controllers 0.2 → 0.3

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: ad259a752520b2a0fbfd467b7f4dd50ee97ba1a2
4
- data.tar.gz: b2a7c7b3d82895b1e66b1677f0137c6d91947503
3
+ metadata.gz: f549f005c93de6eb099efb11b80a429c99077cf0
4
+ data.tar.gz: 44dd3003008e227b826fff4c51b3c1e9d28cf9d7
5
5
  SHA512:
6
- metadata.gz: 5d276829328864ccc91cda85e6b7c866a7e22b397fe598645eb0b5b2e0414c9bf23b54b13233951f56e156221a351d1ecd753be7339cd28ba1e016f6d8696fee
7
- data.tar.gz: cc2d9d76498663038ea08a401cff03672da41115cfa1aa35b9f4ac4530129387225d699351e91f46d5b686c3411e321653f7c2b1e755fa79ffc5fdf717f59e9a
6
+ metadata.gz: 975ae25a383bee2c3b88d79b126c95be07c379cf69382ce24666ef0963cd89a6e312fa8cd530944f784c0ec63599b2afcdc294678cfe9f13f58bd4c649ee5c6d
7
+ data.tar.gz: c0a2877d7e19e90deaa5c5e43f54222ffc256fd2a7b1487ad79fd149011504288ffea052e8510bd9dd164bff20dd319f9e940727277a1d791a01ee683af92e15
data/README.md CHANGED
@@ -42,7 +42,7 @@ The above does a multitude of assumptions to make sure that you can type the lea
42
42
  3. Your model responds to `find`, and `where`
43
43
  4. Your model responds to `is_accessible_to?`. This can be changed at `SkinnyControllers.accessible_to_method`
44
44
 
45
- ### Your model name is different from your resource name
45
+ ### Your model name might be different from your resource name
46
46
  Lets say you have a JSON API resource that you'd like to render that has some additional/subset of data.
47
47
  Maybe the model is an `Event`, and the resource an `EventSummary` (which could do some aggregation of `Event` data).
48
48
 
@@ -128,11 +128,14 @@ SkinnyControllers.controller_namespace = 'API'
128
128
  ```
129
129
 
130
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)
131
+
132
+ |Option|Default|Note|
133
+ |------|-------|----|
134
+ |`operations_namespace` | '' | Optional namespace to put all the operations in. |
135
+ |`operations_suffix`|`'Operations'` | Default suffix for the operations namespaces. |
136
+ |`policy_suffix`|`'Policy'` | Default suffix for policies classes. |
137
+ |`controller_namespace`|`''`| Global Namespace for all controllers (e.g.: `'API'`) |
138
+ |`allow_by_default`| `true` | Default permission |
139
+ |`accessible_to_method`|`is_accessible_to?`| method to call an the object that the user might be able to access |
140
+ |`accessible_to_scope`| `accessible_to`| scope / class method on an object that the user might be able to access |
141
+ |`action_map`| see [skinny_controllers.rb](./lib/skinny_controllers.rb#L61)| |
@@ -2,6 +2,10 @@ module SkinnyControllers
2
2
  module Diet
3
3
  extend ActiveSupport::Concern
4
4
 
5
+ included do
6
+ cattr_accessor :model_class
7
+ end
8
+
5
9
  # TODO: what if we want multiple operations per action?
6
10
  #
7
11
  # @return an instance of the operation with default parameters
@@ -12,73 +16,27 @@ module SkinnyControllers
12
16
  # Assumes the operation name from the controller name
13
17
  #
14
18
  # @example SomeObjectsController => Operation::SomeObject::Action
15
- #
19
+ # @return [Class] the operation class for the model and verb
16
20
  def operation_class
17
- model_name = model_name_from_controller
18
- klass_name = operation_class_from_model(model_name)
19
- klass = klass_name.safe_constantize
20
- klass || default_operation_class_for(model_name)
21
- end
22
-
23
- # dynamically creates a module for the model if it
24
- # isn't already defined
25
- def default_operation_class_for(model_name)
26
- default_operation = Operation::Default
27
- namespace = default_operation_namespace_for(model_name)
28
-
29
- default = "#{namespace.name}::Default".safe_constantize
30
- default || namespace.const_set('Default'.freeze, default_operation.dup)
31
- end
32
-
33
- def default_operation_namespace_for(model_name)
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)
21
+ Lookup::Operation.from_controller(self.class.name, verb_for_action, model_class)
39
22
  end
40
23
 
41
24
  # abstraction for `operation.run`
42
25
  # useful when there is no logic needed for deciding what to
43
26
  # do with an operation or if there is no logic to decide which operation
44
27
  # to use
28
+ #
29
+ # @return [ActiveRecord::Base] the model
45
30
  def model
46
31
  @model ||= operation.run
47
32
  end
48
33
 
49
34
  private
50
35
 
51
- def operation_namespace_from_model(model_name)
52
- "#{model_name}#{SkinnyControllers::operations_suffix}"
53
- end
54
-
55
36
  # action name is inherited from ActionController::Base
56
37
  # http://www.rubydoc.info/docs/rails/2.3.8/ActionController%2FBase%3Aaction_name
57
38
  def verb_for_action
58
39
  SkinnyControllers.action_map[action_name] || SkinnyControllers.action_map['default']
59
40
  end
60
-
61
- def model_name_from_controller
62
- object_name = resource_name_from_controller.singularize
63
- # remove the namespace if one exists
64
- object_name.slice! controller_name_prefix
65
- object_name
66
- end
67
-
68
- def resource_name_from_controller
69
- controller_name = self.class.name
70
- controller_name.gsub('Controller', '')
71
- end
72
-
73
- def operation_class_from_model(model_name)
74
- prefix = SkinnyControllers.operations_namespace
75
- namespace = operation_namespace_from_model(model_name)
76
- "#{prefix}::#{namespace}::#{verb_for_action}"
77
- end
78
-
79
- def controller_name_prefix
80
- namespace = SkinnyControllers.controller_namespace || ''
81
- "#{namespace}::" if namespace
82
- end
83
41
  end
84
42
  end
@@ -0,0 +1,38 @@
1
+ module SkinnyControllers
2
+ module Lookup
3
+ module Controller
4
+ module_function
5
+
6
+ # @example ObjectsController => Objects
7
+ # @return [String] the resource name
8
+ def resource_name(controller_name)
9
+ controller_name.gsub('Controller', '')
10
+ end
11
+
12
+ # @example <ObjectsContreller> => Object
13
+ # @return [String] name of the class of the model
14
+ def model_name(controller)
15
+ resource_name = Controller.resource_name(controller)
16
+ # Convert Resources to Resource
17
+ object_name = resource_name.singularize
18
+ # remove the namespace if one exists
19
+ object_name.slice! namespace
20
+ object_name
21
+ end
22
+
23
+ # TODO: add option to configure this per controller
24
+ #
25
+ # @example
26
+ # If the controller_namespace is specified as 'API', and
27
+ # the controller name is API::ObjectsController,
28
+ # API:: will be ignored from the name. However if the
29
+ # controller_namespace is left blank, API will be assumed to
30
+ # be a part of the model's namespace.
31
+ # @return [String] the optional namespace of all controllers
32
+ def namespace
33
+ namespace = SkinnyControllers.controller_namespace || ''
34
+ "#{namespace}::" if namespace
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,33 @@
1
+ module SkinnyControllers
2
+ module Lookup
3
+ module Model
4
+ module_function
5
+
6
+ # @example 'ObjectOperations::Verb' => Object
7
+ #
8
+ # @return [Class] class based on the operation
9
+ def class_from_operation(operation_name)
10
+ # "Namespace::Model" => "Model"
11
+ model_name = Model.name_from_operation(operation_name)
12
+ # object_type_of_interest.demodulize
13
+
14
+ # "Model" => Model
15
+ model_name.constantize
16
+ end
17
+
18
+ # @example 'Namespace::ModelOperation::Verb' => 'Model'
19
+ # @return [String] the model name corresponding to the operation
20
+ def name_from_operation(operation_name)
21
+ # operation_name is something of the form:
22
+ # Namespace::ModelOperations::Verb
23
+
24
+ # Namespace::ModelOperations::Verb => Namespace::ModelOperations
25
+ namespace = operation_name.deconstantize
26
+ # Namespace::ModelOperations => ModelOperations
27
+ nested_namespace = namespace.demodulize
28
+ # ModelOperations => Model
29
+ nested_namespace.gsub(SkinnyControllers.operations_suffix, '')
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,65 @@
1
+ module SkinnyControllers
2
+ module Lookup
3
+ module Operation
4
+ module_function
5
+
6
+ # @example ObjectOperations::Verb
7
+ #
8
+ # @param [String] model_name name of the model
9
+ # @param [String] verb the verb/action for the operation
10
+ # @return [Class] the operation based on the model name and the verb
11
+ def operation_of(model_name, verb)
12
+ klass_name = Lookup::Operation.name_from_model(model_name, verb)
13
+ klass_name.safe_constantize
14
+ end
15
+
16
+ # @param [String] controller name of the controller class
17
+ # @param [String] verb
18
+ # @param [String] model_name optional
19
+ # @return [Class] the class or default
20
+ def from_controller(controller, verb, model_name = nil)
21
+ model_name ||= Lookup::Controller.model_name(controller)
22
+ klass = operation_of(model_name, verb)
23
+ klass || default_operation_class_for(model_name)
24
+ end
25
+
26
+ # dynamically creates a module for the model if it
27
+ # isn't already defined
28
+ # @return [Class] default operation class
29
+ def default_operation_class_for(model_name)
30
+ default_operation = SkinnyControllers::Operation::Default
31
+ namespace = Lookup::Operation.default_operation_namespace_for(model_name)
32
+
33
+ default = "#{namespace.name}::Default".safe_constantize
34
+ default || namespace.const_set('Default'.freeze, default_operation.dup)
35
+ end
36
+
37
+ # @return [Class] namespace for the default operation class
38
+ def default_operation_namespace_for(model_name)
39
+ desired_namespace = namespace_from_model(model_name)
40
+
41
+ parent_namespace = SkinnyControllers.operations_namespace
42
+ namespace = "#{parent_namespace}::#{desired_namespace}".safe_constantize
43
+ namespace || Object.const_set(desired_namespace, Module.new)
44
+ end
45
+
46
+ # @example 'Object' => 'ObjectOperations'
47
+ # @return [String] the operation namespace based on the model name
48
+ def namespace_from_model(model_name)
49
+ "#{model_name}#{SkinnyControllers.operations_suffix}"
50
+ end
51
+
52
+ # @example 'Model', 'Verb' => [optional namespace]::ModelOperations::Verb
53
+ #
54
+ # @param [String] model_name name of the model
55
+ # @param [String] the verb/action for the operation
56
+ # @return [String] the operation based on the model name
57
+ def name_from_model(model_name, verb)
58
+ # this namespace is '' by default
59
+ prefix = SkinnyControllers.operations_namespace
60
+ namespace = Lookup::Operation.namespace_from_model(model_name)
61
+ "#{prefix}::#{namespace}::#{verb}"
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,25 @@
1
+ module SkinnyControllers
2
+ module Lookup
3
+ module Policy
4
+ module_function
5
+
6
+ # @param [String] name the name of the model
7
+ # @return [Class] the policy class
8
+ def class_from_model(name)
9
+ (
10
+ "#{namespace}::#{name}" +
11
+ SkinnyControllers.policy_suffix
12
+ ).constantize
13
+ end
14
+
15
+ # @param [String] class_name name of the operation class
16
+ def method_name_for_operation(class_name)
17
+ class_name.demodulize.downcase + POLICY_METHOD_SUFFIX
18
+ end
19
+
20
+ def namespace
21
+ SkinnyControllers.policies_namespace
22
+ end
23
+ end
24
+ end
25
+ end
@@ -3,8 +3,8 @@ module SkinnyControllers
3
3
  #
4
4
  # An example Operation may looy like
5
5
  #
6
- # module Operations
7
- # class Event::Read < Base
6
+ # module EventOperations
7
+ # class Read < SkinnyControllers::Policy::Base
8
8
  # def run
9
9
  # model if allowed?
10
10
  # end
@@ -13,13 +13,12 @@ module SkinnyControllers
13
13
  #
14
14
  # TODO: make the above the 'default' and not require to be defined
15
15
  class Base
16
- include PolicyHelpers
17
16
  include ModelHelpers
18
17
 
19
18
  attr_accessor :params, :current_user, :authorized_via_parent
20
19
 
21
20
  def self.run(current_user, params)
22
- object = self.new(current_user, params)
21
+ object = new(current_user, params)
23
22
  object.run
24
23
  end
25
24
 
@@ -41,34 +40,47 @@ module SkinnyControllers
41
40
  end
42
41
 
43
42
  def object_class
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
43
+ @object_class ||= Lookup::Model.class_from_operation(self.class.name)
52
44
  end
53
45
 
54
46
  def object_type_of_interest
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
47
+ @object_type_name ||= Lookup::Model.name_from_operation(self.class.name)
67
48
  end
68
49
 
69
50
  def association_name_from_object
70
51
  object_type_of_interest.tableize
71
52
  end
53
+
54
+ # Takes the class name of self and converts it to a Policy class name
55
+ #
56
+ # @example In Operation::Event::Read, Policy::EventPolicy is returned
57
+ def policy_class
58
+ @policy_class ||= Lookup::Policy.class_from_model(object_type_of_interest)
59
+ end
60
+
61
+ # Converts the class name to the method name to call on the policy
62
+ #
63
+ # @example Operation::Event::Read would become read?
64
+ def policy_method_name
65
+ @policy_method_name ||= Lookup::Policy.method_name_for_operation(self.class.name)
66
+ end
67
+
68
+ # @return a new policy object and caches it
69
+ def policy_for(object)
70
+ @policy ||= policy_class.new(
71
+ current_user,
72
+ object,
73
+ authorized_via_parent: authorized_via_parent)
74
+ end
75
+
76
+ def allowed?
77
+ allowed_for?(model)
78
+ end
79
+
80
+ # checks the policy
81
+ def allowed_for?(object)
82
+ policy_for(object).send(policy_method_name)
83
+ end
72
84
  end
73
85
  end
74
86
  end
@@ -4,8 +4,6 @@ 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
-
9
7
  @model ||=
10
8
  if id_from_params
11
9
  model_from_id
@@ -27,8 +25,8 @@ module SkinnyControllers
27
25
  def scoped_model(scoped_params)
28
26
  unless @scoped_model
29
27
  klass_name = scoped_params[:type]
30
- operation_name = operation_for(klass_name, 'Read'.freeze)
31
- operation = operation_name.constantize.new(current_user, id: scoped_params[:id])
28
+ operation_class = Lookup::Operation.operation_of(klass_name, DefaultVerbs::Read)
29
+ operation = operation_class.new(current_user, id: scoped_params[:id])
32
30
  @scoped_model = operation.run
33
31
  self.authorized_via_parent = !!@scoped_model
34
32
  end
@@ -36,22 +34,13 @@ module SkinnyControllers
36
34
  @scoped_model
37
35
  end
38
36
 
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
-
51
37
  def model_from_params
52
38
  ar_proxy = object_class.where(sanitized_params)
53
39
 
54
40
  if ar_proxy.respond_to? SkinnyControllers.accessible_to_scope
41
+ # It's better to filter in sql, than in the app, so if there is
42
+ # a way to do the filtering in active query, do that. This will help
43
+ # mitigate n+1 query scenarios
55
44
  return ar_proxy.accessible_to(current_user)
56
45
  end
57
46
 
@@ -1,3 +1,3 @@
1
1
  module SkinnyControllers
2
- VERSION = 0.2
2
+ VERSION = 0.3
3
3
  end
@@ -7,21 +7,20 @@ require 'active_support/core_ext/string/inflections'
7
7
 
8
8
  # files for this gem
9
9
  require 'skinny_controllers/default_verbs'
10
+ require 'skinny_controllers/lookup/controller'
11
+ require 'skinny_controllers/lookup/model'
12
+ require 'skinny_controllers/lookup/operation'
13
+ require 'skinny_controllers/lookup/policy'
10
14
  require 'skinny_controllers/policy/base'
11
- require 'skinny_controllers/operation/policy_helpers'
12
15
  require 'skinny_controllers/operation/model_helpers'
13
16
  require 'skinny_controllers/operation/base'
14
17
  require 'skinny_controllers/operation/default'
15
18
  require 'skinny_controllers/diet'
16
19
  require 'skinny_controllers/version'
17
20
 
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
-
24
21
  module SkinnyControllers
22
+ POLICY_METHOD_SUFFIX = '?'.freeze
23
+
25
24
  # Tells the Diet what namespace of the controller
26
25
  # isn't going to be part of the model name
27
26
  #
@@ -32,11 +31,11 @@ module SkinnyControllers
32
31
  cattr_accessor :controller_namespace
33
32
 
34
33
  cattr_accessor :operations_suffix do
35
- 'Operations'
34
+ 'Operations'.freeze
36
35
  end
37
36
 
38
37
  cattr_accessor :policy_suffix do
39
- 'Policy'
38
+ 'Policy'.freeze
40
39
  end
41
40
 
42
41
  cattr_accessor :operations_namespace do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: skinny_controllers
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.2'
4
+ version: '0.3'
5
5
  platform: ruby
6
6
  authors:
7
7
  - L. Preston Sego III
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-11-17 00:00:00.000000000 Z
11
+ date: 2015-11-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -189,10 +189,13 @@ files:
189
189
  - lib/skinny_controllers.rb
190
190
  - lib/skinny_controllers/default_verbs.rb
191
191
  - lib/skinny_controllers/diet.rb
192
+ - lib/skinny_controllers/lookup/controller.rb
193
+ - lib/skinny_controllers/lookup/model.rb
194
+ - lib/skinny_controllers/lookup/operation.rb
195
+ - lib/skinny_controllers/lookup/policy.rb
192
196
  - lib/skinny_controllers/operation/base.rb
193
197
  - lib/skinny_controllers/operation/default.rb
194
198
  - lib/skinny_controllers/operation/model_helpers.rb
195
- - lib/skinny_controllers/operation/policy_helpers.rb
196
199
  - lib/skinny_controllers/policy/base.rb
197
200
  - lib/skinny_controllers/version.rb
198
201
  homepage: https://github.com/NullVoxPopuli/skinny-controllers
@@ -218,5 +221,5 @@ rubyforge_project:
218
221
  rubygems_version: 2.4.8
219
222
  signing_key:
220
223
  specification_version: 4
221
- summary: SkinnyControllers-0.2
224
+ summary: SkinnyControllers-0.3
222
225
  test_files: []
@@ -1,49 +0,0 @@
1
- module SkinnyControllers
2
- module Operation
3
- module PolicyHelpers
4
- POLICY_METHOD_SUFFIX = '?'.freeze
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
9
- def policy_class
10
- @policy_class ||= (
11
- policy_class_namespace +
12
- object_type_of_interest +
13
- SkinnyControllers.policy_suffix
14
- ).constantize
15
- end
16
-
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
22
- end
23
-
24
- # @return a new policy object and caches it
25
- def policy_for(object)
26
- @policy ||= policy_class.new(
27
- current_user,
28
- object,
29
- authorized_via_parent: authorized_via_parent)
30
- end
31
-
32
- def allowed?
33
- allowed_for?(model)
34
- end
35
-
36
- # checks the policy
37
- def allowed_for?(object)
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
46
- end
47
- end
48
- end
49
- end