skinny_controllers 0.1
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 +7 -0
- data/README.md +23 -0
- data/lib/skinny_controllers.rb +54 -0
- data/lib/skinny_controllers/default_verbs.rb +14 -0
- data/lib/skinny_controllers/diet.rb +77 -0
- data/lib/skinny_controllers/operation/base.rb +57 -0
- data/lib/skinny_controllers/operation/default.rb +9 -0
- data/lib/skinny_controllers/operation/model_helpers.rb +59 -0
- data/lib/skinny_controllers/operation/policy_helpers.rb +37 -0
- data/lib/skinny_controllers/policy/base.rb +32 -0
- data/lib/skinny_controllers/version.rb +3 -0
- metadata +152 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 22e75d7a70d070311ace3af0a33cfa1df96dce4d
|
4
|
+
data.tar.gz: cee31b4824b93e8d425746a443212c1f36ca7f31
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0e1c99dd3bfd118eac0cb5081327328bf0dfbee23a5f93cee0a2d9fdf9f30d22eebab32aa0e7b6329a903ba88788a3e488893f12a6c0311f6153bc3a837adb61
|
7
|
+
data.tar.gz: e8ad57f6833d21b4148f8f5307e262d072698f83d114a027813297e7ad831df38afcce29436964077814935c5c3fc590855c4b2ca8335da1643cdd15800873e7
|
data/README.md
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# skinny-controllers
|
2
|
+
[](http://badge.fury.io/rb/skinny_controllers)
|
3
|
+
[](https://travis-ci.org/NullVoxPopuli/skinny_controllers)
|
4
|
+
[](https://codeclimate.com/github/NullVoxPopuli/skinny_controllers)
|
5
|
+
[](https://codeclimate.com/github/NullVoxPopuli/skinny_controllers)
|
6
|
+
[](https://gemnasium.com/NullVoxPopuli/skinny_controllers)
|
7
|
+
|
8
|
+
An implementation of role-based policies and operations to help controllers lose weight.
|
9
|
+
|
10
|
+
The goal of this project is to help API apps be more slim, and separate logic as much as possible.
|
11
|
+
|
12
|
+
# Installation
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
gem 'skinny_controllers'
|
16
|
+
```
|
17
|
+
or
|
18
|
+
|
19
|
+
`gem install skinny_controllers`
|
20
|
+
|
21
|
+
# Usage
|
22
|
+
|
23
|
+
TODO: this section
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# required gems
|
2
|
+
require 'active_support'
|
3
|
+
require 'active_support/core_ext/class'
|
4
|
+
require 'active_support/core_ext/object/blank'
|
5
|
+
require 'active_support/core_ext/object/try'
|
6
|
+
require 'active_support/core_ext/string/inflections'
|
7
|
+
|
8
|
+
# files for this gem
|
9
|
+
require 'skinny_controllers/default_verbs'
|
10
|
+
require 'skinny_controllers/policy/base'
|
11
|
+
require 'skinny_controllers/operation/policy_helpers'
|
12
|
+
require 'skinny_controllers/operation/model_helpers'
|
13
|
+
require 'skinny_controllers/operation/base'
|
14
|
+
require 'skinny_controllers/operation/default'
|
15
|
+
require 'skinny_controllers/diet'
|
16
|
+
require 'skinny_controllers/version'
|
17
|
+
|
18
|
+
module SkinnyControllers
|
19
|
+
# Tells the Diet what namespace of the controller
|
20
|
+
# isn't going to be part of the model name
|
21
|
+
#
|
22
|
+
# @example
|
23
|
+
# # config/initializers/skinny_controllers.rb
|
24
|
+
# SkinnyControllers.controller_namespace = 'API'
|
25
|
+
# # 'API::' would be removed from 'API::Namespace::ObjectNamesController'
|
26
|
+
cattr_accessor :controller_namespace
|
27
|
+
|
28
|
+
#
|
29
|
+
cattr_accessor :operation_namespace do
|
30
|
+
'Operation'.freeze
|
31
|
+
end
|
32
|
+
|
33
|
+
cattr_accessor :allow_by_default do
|
34
|
+
true
|
35
|
+
end
|
36
|
+
|
37
|
+
# the diet uses ActionController::Base's
|
38
|
+
# `action_name` method to get the current action.
|
39
|
+
# From that action, we map what verb we want to use for our operation
|
40
|
+
#
|
41
|
+
# If an action is not be listed, the action name will be
|
42
|
+
# manipulated to fit the verb naming convention.
|
43
|
+
#
|
44
|
+
# @example POST controller#send_receipt will use 'SendReceipt' for the verb
|
45
|
+
cattr_accessor :action_map do
|
46
|
+
{
|
47
|
+
'default'.freeze => DefaultVerbs::Read,
|
48
|
+
'create'.freeze => DefaultVerbs::Create,
|
49
|
+
'index'.freeze => DefaultVerbs::ReadAll,
|
50
|
+
'destroy'.freeze => DefaultVerbs::Delete,
|
51
|
+
'update'.freeze => DefaultVerbs::Update
|
52
|
+
}
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module SkinnyControllers
|
2
|
+
module Diet
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
# TODO: what if we want multiple operations per action?
|
6
|
+
#
|
7
|
+
# @return an instance of the operation with default parameters
|
8
|
+
def operation
|
9
|
+
@operation ||= operation_class.new(current_user, params)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Assumes the operation name from the controller name
|
13
|
+
#
|
14
|
+
# @example SomeObjectsController => Operation::SomeObject::Action
|
15
|
+
#
|
16
|
+
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
|
+
parent_namespace = Operation::Default.name.deconstantize
|
35
|
+
namespace = "#{parent_namespace}::#{model_name}".safe_constantize
|
36
|
+
namespace || Operation.const_set(model_name, Module.new)
|
37
|
+
end
|
38
|
+
|
39
|
+
# abstraction for `operation.run`
|
40
|
+
# useful when there is no logic needed for deciding what to
|
41
|
+
# do with an operation or if there is no logic to decide which operation
|
42
|
+
# to use
|
43
|
+
def model
|
44
|
+
@model ||= operation.run
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
# action name is inherited from ActionController::Base
|
50
|
+
# http://www.rubydoc.info/docs/rails/2.3.8/ActionController%2FBase%3Aaction_name
|
51
|
+
def verb_for_action
|
52
|
+
SkinnyControllers.action_map[action_name] || SkinnyControllers.action_map['default']
|
53
|
+
end
|
54
|
+
|
55
|
+
def model_name_from_controller
|
56
|
+
object_name = resource_name_from_controller.singularize
|
57
|
+
# remove the namespace if one exists
|
58
|
+
object_name.slice! controller_name_prefix
|
59
|
+
object_name
|
60
|
+
end
|
61
|
+
|
62
|
+
def resource_name_from_controller
|
63
|
+
controller_name = self.class.name
|
64
|
+
controller_name.gsub('Controller', '')
|
65
|
+
end
|
66
|
+
|
67
|
+
def operation_class_from_model(model_name)
|
68
|
+
prefix = SkinnyControllers.operation_namespace
|
69
|
+
"#{prefix}::#{model_name}::#{verb_for_action}"
|
70
|
+
end
|
71
|
+
|
72
|
+
def controller_name_prefix
|
73
|
+
namespace = SkinnyControllers.controller_namespace || ''
|
74
|
+
"#{namespace}::" if namespace
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module SkinnyControllers
|
2
|
+
module Operation
|
3
|
+
#
|
4
|
+
# An example Operation may looy like
|
5
|
+
#
|
6
|
+
# module Operations
|
7
|
+
# class Event::Read < Base
|
8
|
+
# def run
|
9
|
+
# model if allowed?
|
10
|
+
# end
|
11
|
+
# end
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# TODO: make the above the 'default' and not require to be defined
|
15
|
+
class Base
|
16
|
+
include PolicyHelpers
|
17
|
+
include ModelHelpers
|
18
|
+
|
19
|
+
attr_accessor :params, :current_user, :authorized_via_parent
|
20
|
+
|
21
|
+
class << self
|
22
|
+
def run(current_user, params)
|
23
|
+
new(current_user, params).run
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(current_user, params)
|
28
|
+
self.current_user = current_user
|
29
|
+
self.params = params
|
30
|
+
self.authorized_via_parent = false
|
31
|
+
end
|
32
|
+
|
33
|
+
def id_from_params
|
34
|
+
unless @id_from_params
|
35
|
+
@id_from_params = params[:id]
|
36
|
+
if filter = params[:filter]
|
37
|
+
@id_from_params = filter[:id].split(',')
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
@id_from_params
|
42
|
+
end
|
43
|
+
|
44
|
+
def object_class
|
45
|
+
@object_class ||= object_type_of_interest.demodulize.constantize
|
46
|
+
end
|
47
|
+
|
48
|
+
def object_type_of_interest
|
49
|
+
@object_type_name ||= self.class.name.deconstantize.demodulize
|
50
|
+
end
|
51
|
+
|
52
|
+
def association_name_from_object
|
53
|
+
object_type_of_interest.tableize
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module SkinnyControllers
|
2
|
+
module Operation
|
3
|
+
module ModelHelpers
|
4
|
+
def model
|
5
|
+
# TODO: not sure if multiple ids is a good idea here
|
6
|
+
# if we don't have a(ny) id(s), get all of them
|
7
|
+
@model ||=
|
8
|
+
if id_from_params
|
9
|
+
model_from_id
|
10
|
+
elsif params[:scope]
|
11
|
+
model_from_scope
|
12
|
+
elsif key = params.keys.grep(/_id$/)
|
13
|
+
# hopefully there is only ever one of these passed
|
14
|
+
model_from_named_id(key.first)
|
15
|
+
else
|
16
|
+
model_from_params
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def scoped_model(scoped_params)
|
21
|
+
unless @scoped_model
|
22
|
+
klass_name = scoped_params[:type]
|
23
|
+
operation_name = "Operations::#{klass_name}::Read"
|
24
|
+
operation = operation_name.constantize.new(current_user, id: scoped_params[:id])
|
25
|
+
@scoped_model = operation.run
|
26
|
+
self.authorized_via_parent = !!@scoped_model
|
27
|
+
end
|
28
|
+
|
29
|
+
@scoped_model
|
30
|
+
end
|
31
|
+
|
32
|
+
def model_from_params
|
33
|
+
object_class.where(params).accessible_to(current_user)
|
34
|
+
end
|
35
|
+
|
36
|
+
def model_from_named_id(key)
|
37
|
+
name, id = key.split('_')
|
38
|
+
name = name.camelize
|
39
|
+
model_from_scope(
|
40
|
+
id: id,
|
41
|
+
type: name
|
42
|
+
)
|
43
|
+
end
|
44
|
+
|
45
|
+
def model_from_scope(scope = params[:scope])
|
46
|
+
if scoped = scoped_model(scope)
|
47
|
+
association = association_name_from_object
|
48
|
+
scoped.send(association)
|
49
|
+
else
|
50
|
+
fail "Parent object of type #{scope[:type]} not accessible"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def model_from_id
|
55
|
+
object_class.find(id_from_params)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module SkinnyControllers
|
2
|
+
module Operation
|
3
|
+
module PolicyHelpers
|
4
|
+
POLICY_CLASS_PREFIX = 'Policy::'.freeze
|
5
|
+
POLICY_CLASS_SUFFIX = 'Policy'.freeze
|
6
|
+
POLICY_SUFFIX = '?'.freeze
|
7
|
+
|
8
|
+
def policy_class
|
9
|
+
@policy_class ||= (
|
10
|
+
POLICY_CLASS_PREFIX +
|
11
|
+
object_type_of_interest +
|
12
|
+
POLICY_CLASS_SUFFIX
|
13
|
+
).constantize
|
14
|
+
end
|
15
|
+
|
16
|
+
def policy_name
|
17
|
+
@policy_name ||= self.class.name.demodulize.downcase + POLICY_SUFFIX
|
18
|
+
end
|
19
|
+
|
20
|
+
def policy_for(object)
|
21
|
+
@policy ||= policy_class.new(
|
22
|
+
current_user,
|
23
|
+
object,
|
24
|
+
authorized_via_parent: authorized_via_parent)
|
25
|
+
end
|
26
|
+
|
27
|
+
def allowed?
|
28
|
+
policy_for(model)
|
29
|
+
end
|
30
|
+
|
31
|
+
# checks the policy
|
32
|
+
def allowed_for?(object)
|
33
|
+
policy_for(object).send(policy_name)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module SkinnyControllers
|
2
|
+
module Policy
|
3
|
+
class Base
|
4
|
+
attr_accessor :user, :object, :authorized_via_parent
|
5
|
+
|
6
|
+
def initialize(user, object, authorized_via_parent: false)
|
7
|
+
self.user = user
|
8
|
+
self.object = object
|
9
|
+
self.authorized_via_parent = authorized_via_parent
|
10
|
+
end
|
11
|
+
|
12
|
+
def default?
|
13
|
+
SkinnyControllers.allow_by_default
|
14
|
+
end
|
15
|
+
|
16
|
+
# defaults
|
17
|
+
def read?(o = object)
|
18
|
+
o.is_accessible_to? user
|
19
|
+
end
|
20
|
+
|
21
|
+
def read_all?
|
22
|
+
return true if authorized_via_parent
|
23
|
+
# This is expensive, so try to avoid it
|
24
|
+
# TODO: look in to creating a cache for
|
25
|
+
# these look ups that's invalidated upon
|
26
|
+
# object save
|
27
|
+
accessible = object.map { |ea| read?(ea) }
|
28
|
+
accessible.all?
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
metadata
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: skinny_controllers
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.1'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- L. Preston Sego III
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-11-17 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rails
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: awesome_print
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
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: pry-byebug
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: codeclimate-test-reporter
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rubocop
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
description: An implementation of role-based policies and operations to help controllers
|
112
|
+
lose weight.
|
113
|
+
email: LPSego3+dev@gmail.com
|
114
|
+
executables: []
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- README.md
|
119
|
+
- lib/skinny_controllers.rb
|
120
|
+
- lib/skinny_controllers/default_verbs.rb
|
121
|
+
- lib/skinny_controllers/diet.rb
|
122
|
+
- lib/skinny_controllers/operation/base.rb
|
123
|
+
- lib/skinny_controllers/operation/default.rb
|
124
|
+
- lib/skinny_controllers/operation/model_helpers.rb
|
125
|
+
- lib/skinny_controllers/operation/policy_helpers.rb
|
126
|
+
- lib/skinny_controllers/policy/base.rb
|
127
|
+
- lib/skinny_controllers/version.rb
|
128
|
+
homepage: https://github.com/NullVoxPopuli/skinny-controllers
|
129
|
+
licenses:
|
130
|
+
- MIT
|
131
|
+
metadata: {}
|
132
|
+
post_install_message:
|
133
|
+
rdoc_options: []
|
134
|
+
require_paths:
|
135
|
+
- lib
|
136
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
137
|
+
requirements:
|
138
|
+
- - ">="
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
version: '2.0'
|
141
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
requirements: []
|
147
|
+
rubyforge_project:
|
148
|
+
rubygems_version: 2.4.8
|
149
|
+
signing_key:
|
150
|
+
specification_version: 4
|
151
|
+
summary: SkinnyControllers-0.1
|
152
|
+
test_files: []
|