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.
@@ -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