skinny_controllers 0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Gem Version](http://img.shields.io/gem/v/skinny_controllers.svg?style=flat-square)](http://badge.fury.io/rb/skinny_controllers)
|
3
|
+
[![Build Status](http://img.shields.io/travis/NullVoxPopuli/skinny_controllers.svg?style=flat-square)](https://travis-ci.org/NullVoxPopuli/skinny_controllers)
|
4
|
+
[![Code Climate](http://img.shields.io/codeclimate/github/NullVoxPopuli/skinny_controllers.svg?style=flat-square)](https://codeclimate.com/github/NullVoxPopuli/skinny_controllers)
|
5
|
+
[![Test Coverage](http://img.shields.io/codeclimate/coverage/github/NullVoxPopuli/skinny_controllers.svg?style=flat-square)](https://codeclimate.com/github/NullVoxPopuli/skinny_controllers)
|
6
|
+
[![Dependency Status](http://img.shields.io/gemnasium/NullVoxPopuli/skinny_controllers.svg?style=flat-square)](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: []
|