dry-ability 0.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/.gitattributes +2 -0
- data/.gitignore +50 -0
- data/.ruby-version +1 -0
- data/Gemfile +3 -0
- data/LICENSE +21 -0
- data/README.md +20 -0
- data/Rakefile +13 -0
- data/dry-ability.gemspec +36 -0
- data/init.rb +1 -0
- data/lib/dry-ability.rb +1 -0
- data/lib/dry/ability.rb +120 -0
- data/lib/dry/ability/container.rb +43 -0
- data/lib/dry/ability/controller.rb +21 -0
- data/lib/dry/ability/controller/dsl.rb +161 -0
- data/lib/dry/ability/controller/mixin.rb +80 -0
- data/lib/dry/ability/controller/resource.rb +218 -0
- data/lib/dry/ability/controller_resource.rb +62 -0
- data/lib/dry/ability/exceptions.rb +36 -0
- data/lib/dry/ability/f.rb +57 -0
- data/lib/dry/ability/inherited_resource.rb +26 -0
- data/lib/dry/ability/key.rb +19 -0
- data/lib/dry/ability/resource_mediator.rb +94 -0
- data/lib/dry/ability/rule.rb +141 -0
- data/lib/dry/ability/rule_interface.rb +27 -0
- data/lib/dry/ability/rules_builder.rb +92 -0
- data/lib/dry/ability/t.rb +45 -0
- data/lib/dry/ability/version.rb +7 -0
- metadata +214 -0
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/ability/controller/resource"
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module Ability
|
7
|
+
# For use with Inherited Resources
|
8
|
+
class InheritedResource < Controller::Resource # :nodoc:
|
9
|
+
def load_resource_instance
|
10
|
+
if parent?
|
11
|
+
@controller.send :association_chain
|
12
|
+
@controller.instance_variable_get(:"@#{instance_name}")
|
13
|
+
elsif new_actions.include?(@action_name)
|
14
|
+
resource = @controller.send :build_resource
|
15
|
+
assign_attributes(resource)
|
16
|
+
else
|
17
|
+
@controller.send :resource
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def resource_base
|
22
|
+
@controller.send :end_of_association_chain
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Ability
|
5
|
+
class Key < String
|
6
|
+
COERC = F[:coerc_key].to_proc.freeze
|
7
|
+
private_constant :COERC
|
8
|
+
|
9
|
+
attr_reader :nsed
|
10
|
+
alias_method :namespaced, :nsed
|
11
|
+
|
12
|
+
def initialize(key, nsfn)
|
13
|
+
string = COERC[key]
|
14
|
+
@nsed = nsfn[string].freeze
|
15
|
+
super(string)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/initializer"
|
4
|
+
require "dry/ability/t"
|
5
|
+
|
6
|
+
module Dry
|
7
|
+
module Ability
|
8
|
+
class ResourceMediator
|
9
|
+
include Initializer[undefined: false].define -> do
|
10
|
+
defaults = {
|
11
|
+
nil: proc { nil },
|
12
|
+
false: proc { false },
|
13
|
+
with: -> (input, type) { (type.default? ? type.value : [type.member.value]) | type[input] }
|
14
|
+
}.freeze
|
15
|
+
t = { **(t = {
|
16
|
+
bool: T['params.bool'],
|
17
|
+
string: T['params.string'],
|
18
|
+
symbol: T['params.symbol'],
|
19
|
+
array: T['array'] << Array.method(:wrap),
|
20
|
+
set: T.Constructor(Set)
|
21
|
+
}), **{
|
22
|
+
symbols: t[:array].of(T['params.symbol']),
|
23
|
+
symbols_with: -> (*list) { t[:symbols].default(list.freeze) << defaults[:with] }
|
24
|
+
}}.freeze
|
25
|
+
|
26
|
+
param :name, t[:symbol]
|
27
|
+
param :path, t[:string]
|
28
|
+
param :sequence, t[:set] << t[:symbols]
|
29
|
+
|
30
|
+
with_options optional: true do |free|
|
31
|
+
free.option :parent, T['bool']
|
32
|
+
|
33
|
+
free.option :class, T['false'] | t[:string], default: proc { @name.to_s.classify }, as: :class_name
|
34
|
+
|
35
|
+
free.option :shallow, t[:bool], default: defaults[:false]
|
36
|
+
|
37
|
+
free.option :singleton, t[:bool], default: defaults[:false]
|
38
|
+
|
39
|
+
free.option :find_by, t[:symbol]
|
40
|
+
|
41
|
+
free.option :params_method, t[:symbol]
|
42
|
+
|
43
|
+
free.option :through, t[:symbol]
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
|
48
|
+
option :parent_action, t[:symbol], default: proc { :show }
|
49
|
+
|
50
|
+
option :instance_name, t[:symbol], default: proc { @name }
|
51
|
+
|
52
|
+
option :collection_name, t[:symbol], default: proc { @name.to_s.pluralize }
|
53
|
+
|
54
|
+
option :through_association, t[:symbol], default: proc { collection_name }
|
55
|
+
|
56
|
+
option :id_param, t[:symbol], default: proc { parent? ? :"#@name\_id" : :id }, as: :id_param_key
|
57
|
+
|
58
|
+
collection_type = t[:symbols_with][:index]
|
59
|
+
option :collection, collection_type, default: proc { collection_type[] }, as: :collection_actions
|
60
|
+
|
61
|
+
new_type = t[:symbols_with][:new, :create]
|
62
|
+
option :new, new_type, default: proc { new_type[] }, as: :new_actions
|
63
|
+
|
64
|
+
save_type = t[:symbols_with][:create, :update]
|
65
|
+
option :save, save_type, default: proc { save_type[] }, as: :save_actions
|
66
|
+
end
|
67
|
+
|
68
|
+
alias_method :parent?, :parent
|
69
|
+
alias_method :shallow?, :shallow
|
70
|
+
alias_method :singleton?, :singleton
|
71
|
+
|
72
|
+
def before(controller)
|
73
|
+
resource_class(controller).new(self, controller).call
|
74
|
+
end
|
75
|
+
|
76
|
+
def resource_class(controller)
|
77
|
+
if defined?(::InheritedResources) && controller.is_a?(::InheritedResources::Actions)
|
78
|
+
InheritedResource
|
79
|
+
else
|
80
|
+
ControllerResource
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def member_action?(action_name, params)
|
85
|
+
@new_actions.include?(action_name) || singleton? ||
|
86
|
+
((params[:id] || params[@id_param_key]) && !@collection_actions.include?(action_name))
|
87
|
+
end
|
88
|
+
|
89
|
+
def collection_action?(action_name, *)
|
90
|
+
@collection_actions.include?(action_name)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/initializer"
|
4
|
+
require "dry/ability/t"
|
5
|
+
require "dry/ability/rule_interface"
|
6
|
+
|
7
|
+
module Dry
|
8
|
+
module Ability
|
9
|
+
class Rule
|
10
|
+
include Initializer[undefined: false].define -> do
|
11
|
+
param :actions, T::Actions, reader: :private
|
12
|
+
param :subjects, T::Subjects, reader: :private
|
13
|
+
option :inverse, T['bool'], reader: :private
|
14
|
+
option :constraints, T::Hash.map(T['params.symbol'], T['any']), default: -> { {} }
|
15
|
+
option :filter, T::Callable, optional: true
|
16
|
+
option :scope, T::Callable, optional: true
|
17
|
+
option :explicit_scope, T['bool'], default: -> { true }
|
18
|
+
end
|
19
|
+
|
20
|
+
include RuleInterface
|
21
|
+
|
22
|
+
def call(account, object)
|
23
|
+
if filter?
|
24
|
+
filter.(account, object)
|
25
|
+
else
|
26
|
+
@constraints.blank? || run_constraints(account, object, @constraints)
|
27
|
+
end ^ @inverse
|
28
|
+
end
|
29
|
+
|
30
|
+
def attributes_for(account, object)
|
31
|
+
@constraints.blank? ? {} : F[:eval_values, [account, object]][@constraints]
|
32
|
+
end
|
33
|
+
|
34
|
+
def scope_for(account, subject)
|
35
|
+
relation = if scope?
|
36
|
+
@scope.arity > 1 ? @scope[account, subject] : @scope[account]
|
37
|
+
else
|
38
|
+
T::Queriable[subject].all
|
39
|
+
end
|
40
|
+
unless @explicit_scope
|
41
|
+
attrs = attributes_for(account, subject)
|
42
|
+
relation = relation.where(attrs) unless attrs.blank?
|
43
|
+
end
|
44
|
+
relation
|
45
|
+
end
|
46
|
+
|
47
|
+
def filter?
|
48
|
+
!@filter.nil?
|
49
|
+
end
|
50
|
+
|
51
|
+
def scope?
|
52
|
+
!@scope.nil?
|
53
|
+
end
|
54
|
+
|
55
|
+
def accessible?
|
56
|
+
scope? || !@explicit_scope
|
57
|
+
end
|
58
|
+
|
59
|
+
def register_to(_rules)
|
60
|
+
unless defined?(@_registered)
|
61
|
+
@subjects.each do |subject|
|
62
|
+
_rules.namespace(subject) do |_subject|
|
63
|
+
@actions.each do |action|
|
64
|
+
key = _subject.send(:namespaced, action)
|
65
|
+
pred = _rules._container.delete(key)&.call
|
66
|
+
rules_or = pred | self if pred
|
67
|
+
_subject.register action, (rules_or || self)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
@_registered = true
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def |(other)
|
76
|
+
Or.new([self, other])
|
77
|
+
end
|
78
|
+
|
79
|
+
class Or
|
80
|
+
include Dry::Initializer[undefined: false].define -> do
|
81
|
+
param :items, T.Array(T.Instance(Rule))
|
82
|
+
end
|
83
|
+
|
84
|
+
include RuleInterface
|
85
|
+
|
86
|
+
def call(account, object)
|
87
|
+
items.reduce(false) do |result, rule|
|
88
|
+
result || rule[account, object]
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def attributes_for(account, object)
|
93
|
+
items.reduce({}) do |result, rule|
|
94
|
+
result.deep_merge!(rule.attributes_for(rule, object)); result
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def scope_for(account, subject)
|
99
|
+
base_relations = items.map { |rule| rule.scope_for(account, subject) }
|
100
|
+
condit = base_relations.map { |r| r.except(:joins) }.reduce(:or)
|
101
|
+
merged = base_relations.map { |r| r.except(:where) }.reduce(:merge)
|
102
|
+
merged.merge(condit)
|
103
|
+
end
|
104
|
+
|
105
|
+
def accessible?
|
106
|
+
true
|
107
|
+
end
|
108
|
+
|
109
|
+
def |(other)
|
110
|
+
self.class.new([*items, other])
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def run_constraints(account, object, dict)
|
117
|
+
case object when Class, Symbol
|
118
|
+
true
|
119
|
+
else
|
120
|
+
dict.reduce(true) do |pred, (key, value)|
|
121
|
+
pred && call_constraint(account, object, key, value)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def call_constraint(account, object, key, constraint)
|
127
|
+
value = object.public_send(key)
|
128
|
+
case constraint
|
129
|
+
when Array
|
130
|
+
constraint.include?(value)
|
131
|
+
when Hash
|
132
|
+
run_constraints(account, value, constraint)
|
133
|
+
when Proc
|
134
|
+
constraint.arity > 1 ? constraint.(account, value) : value == constraint.(account)
|
135
|
+
else
|
136
|
+
value == constraint
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Ability
|
5
|
+
module RuleInterface
|
6
|
+
def call(account, object)
|
7
|
+
raise NotImplementedError
|
8
|
+
end
|
9
|
+
|
10
|
+
def [](account, object)
|
11
|
+
call(account, object)
|
12
|
+
end
|
13
|
+
|
14
|
+
def |(other)
|
15
|
+
raise NotImplementedError
|
16
|
+
end
|
17
|
+
|
18
|
+
def attributes_for(account)
|
19
|
+
raise NotImplementedError
|
20
|
+
end
|
21
|
+
|
22
|
+
def scope_for(account)
|
23
|
+
raise NotImplementedError
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/initializer"
|
4
|
+
require "dry/ability/t"
|
5
|
+
require "dry/ability/f"
|
6
|
+
require "dry/ability/rule"
|
7
|
+
require "dry/ability/container"
|
8
|
+
|
9
|
+
module Dry
|
10
|
+
module Ability
|
11
|
+
# Creates a container with ability rules and provides DSL to define them.
|
12
|
+
class RulesBuilder
|
13
|
+
include Initializer[undefined: false].define -> do
|
14
|
+
option :_container, T.Instance(Container), default: proc { Container.new }, optional: true
|
15
|
+
end
|
16
|
+
|
17
|
+
# Registers mappings
|
18
|
+
#
|
19
|
+
# @example
|
20
|
+
#
|
21
|
+
# map :action, :read => %i(index show)
|
22
|
+
# map :subject, :public => %w(Post Like Comment)
|
23
|
+
#
|
24
|
+
def map(kind, dict)
|
25
|
+
kind = T::ActionOrSubject[kind]
|
26
|
+
dict = T::RulesMapping[dict]
|
27
|
+
|
28
|
+
@_container.namespace(:mappings) do |_mappings|
|
29
|
+
_mappings.namespace(kind) do |_action_or_subject|
|
30
|
+
dict.each do |mapped, list|
|
31
|
+
list.sort.each do |original|
|
32
|
+
key = _action_or_subject.send(:namespaced, original)
|
33
|
+
pred = Array.wrap(@_container._container.delete(key)&.call)
|
34
|
+
pred << mapped unless pred.include?(mapped)
|
35
|
+
_action_or_subject.register(original, pred)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Shorthand of <tt>map :action, dict</tt>
|
43
|
+
def map_action(dict)
|
44
|
+
map :action, dict
|
45
|
+
end
|
46
|
+
|
47
|
+
# Shorthand of <tt>map :subject, dict</tt>
|
48
|
+
#
|
49
|
+
# @exmaple
|
50
|
+
#
|
51
|
+
# map_subject :public => %w(Post Like Comment)
|
52
|
+
def map_subject(dict)
|
53
|
+
map :subject, dict
|
54
|
+
end
|
55
|
+
|
56
|
+
# Registers rule in the calculated key
|
57
|
+
#
|
58
|
+
# @example
|
59
|
+
#
|
60
|
+
# can :read, :public
|
61
|
+
def can(actions, subjects, filter: nil, scope: nil, inverse: false, explicit_scope: true, **constraints, &block)
|
62
|
+
@_container.namespace(:rules) do |_rules|
|
63
|
+
Rule.new(actions, subjects,
|
64
|
+
constraints: constraints,
|
65
|
+
filter: (filter || block&.to_proc),
|
66
|
+
scope: scope,
|
67
|
+
inverse: inverse,
|
68
|
+
explicit_scope: explicit_scope
|
69
|
+
).register_to(_rules)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# @see #can(*args, **options, &block)
|
74
|
+
def cannot(*args, **options, &block)
|
75
|
+
can(*args, **options, inverse: true, &block)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Generates module, which, after being included into a class, registers singleton
|
79
|
+
# instance variable <tt>@_container</tt> as reference to the composed container of rules.
|
80
|
+
def mixin
|
81
|
+
@mixin ||= Module.new.tap do |mod|
|
82
|
+
container = @_container.freeze
|
83
|
+
mod.define_singleton_method :included do |base|
|
84
|
+
base.instance_variable_set(:@_container, container)
|
85
|
+
super(base)
|
86
|
+
end
|
87
|
+
mod
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/types"
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module Ability
|
7
|
+
# T is for Types
|
8
|
+
module T
|
9
|
+
extend Types::BuilderMethods
|
10
|
+
|
11
|
+
def self.[](*args, &block)
|
12
|
+
Types[*args, &block]
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.Key(input)
|
16
|
+
case input
|
17
|
+
when String, Symbol, Module, Class; input.to_s
|
18
|
+
else Key(input.class)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.WrappedArray(type)
|
23
|
+
Array(type) << ArrayWrap
|
24
|
+
end
|
25
|
+
|
26
|
+
ArrayWrap = Array.method(:wrap)
|
27
|
+
|
28
|
+
CoercKey = Types['string'] << method(:Key)
|
29
|
+
|
30
|
+
Actions = WrappedArray(Types['params.symbol'])
|
31
|
+
|
32
|
+
Subjects = WrappedArray(CoercKey)
|
33
|
+
|
34
|
+
Hash = Types['hash']
|
35
|
+
|
36
|
+
ActionOrSubject = Types['symbol'].enum(:action, :subject)
|
37
|
+
|
38
|
+
RulesMapping = Hash.map(CoercKey, Subjects)
|
39
|
+
|
40
|
+
Callable = Interface(:call)
|
41
|
+
|
42
|
+
Queriable = Interface(:where)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|