action_policy 0.5.0 → 0.5.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 +4 -4
- data/lib/.rbnext/2.7/action_policy/behaviours/policy_for.rb +62 -0
- data/lib/.rbnext/2.7/action_policy/i18n.rb +56 -0
- data/lib/.rbnext/2.7/action_policy/policy/cache.rb +101 -0
- data/lib/.rbnext/2.7/action_policy/policy/pre_check.rb +162 -0
- data/lib/.rbnext/2.7/action_policy/rspec/be_authorized_to.rb +89 -0
- data/lib/.rbnext/2.7/action_policy/rspec/have_authorized_scope.rb +124 -0
- data/lib/.rbnext/2.7/action_policy/utils/pretty_print.rb +159 -0
- data/lib/.rbnext/3.0/action_policy/behaviour.rb +115 -0
- data/lib/.rbnext/3.0/action_policy/behaviours/policy_for.rb +62 -0
- data/lib/.rbnext/3.0/action_policy/behaviours/scoping.rb +35 -0
- data/lib/.rbnext/3.0/action_policy/behaviours/thread_memoized.rb +59 -0
- data/lib/.rbnext/3.0/action_policy/ext/policy_cache_key.rb +72 -0
- data/lib/.rbnext/3.0/action_policy/policy/aliases.rb +69 -0
- data/lib/.rbnext/3.0/action_policy/policy/authorization.rb +87 -0
- data/lib/.rbnext/3.0/action_policy/policy/cache.rb +101 -0
- data/lib/.rbnext/3.0/action_policy/policy/core.rb +161 -0
- data/lib/.rbnext/3.0/action_policy/policy/defaults.rb +31 -0
- data/lib/.rbnext/3.0/action_policy/policy/execution_result.rb +37 -0
- data/lib/.rbnext/3.0/action_policy/policy/pre_check.rb +162 -0
- data/lib/.rbnext/3.0/action_policy/policy/reasons.rb +210 -0
- data/lib/.rbnext/3.0/action_policy/policy/scoping.rb +160 -0
- data/lib/.rbnext/3.0/action_policy/rspec/be_authorized_to.rb +89 -0
- data/lib/.rbnext/3.0/action_policy/rspec/have_authorized_scope.rb +124 -0
- data/lib/.rbnext/3.0/action_policy/utils/pretty_print.rb +159 -0
- data/lib/.rbnext/3.0/action_policy/utils/suggest_message.rb +19 -0
- data/lib/action_policy/version.rb +1 -1
- metadata +27 -2
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionPolicy
|
4
|
+
module Policy
|
5
|
+
# Create default rules and aliases:
|
6
|
+
# - `index?` (=`false`)
|
7
|
+
# - `create?` (=`false`)
|
8
|
+
# - `new?` as an alias for `create?`
|
9
|
+
# - `manage?` as a fallback for all unspecified rules (default rule)
|
10
|
+
module Defaults
|
11
|
+
def self.included(base)
|
12
|
+
raise "Aliases support is required for defaults" unless
|
13
|
+
base.ancestors.include?(Aliases)
|
14
|
+
|
15
|
+
base.default_rule :manage?
|
16
|
+
base.alias_rule :new?, to: :create?
|
17
|
+
|
18
|
+
raise "Verification context support is required for defaults" unless
|
19
|
+
base.ancestors.include?(Aliases)
|
20
|
+
|
21
|
+
base.authorize :user
|
22
|
+
end
|
23
|
+
|
24
|
+
def index?() ; false; end
|
25
|
+
|
26
|
+
def create?() ; false; end
|
27
|
+
|
28
|
+
def manage?() ; false; end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionPolicy
|
4
|
+
module Policy
|
5
|
+
# Result of applying a policy rule
|
6
|
+
#
|
7
|
+
# This class could be extended by some modules to provide
|
8
|
+
# additional functionality
|
9
|
+
class ExecutionResult
|
10
|
+
attr_reader :value, :policy, :rule
|
11
|
+
|
12
|
+
def initialize(policy, rule)
|
13
|
+
@policy = policy
|
14
|
+
@rule = rule
|
15
|
+
end
|
16
|
+
|
17
|
+
# Populate the final value
|
18
|
+
def load(value)
|
19
|
+
@value = value
|
20
|
+
end
|
21
|
+
|
22
|
+
def success?() ; @value == true; end
|
23
|
+
|
24
|
+
def fail?() ; @value == false; end
|
25
|
+
|
26
|
+
def cached!
|
27
|
+
@cached = true
|
28
|
+
end
|
29
|
+
|
30
|
+
def cached?() ; @cached == true; end
|
31
|
+
|
32
|
+
def inspect
|
33
|
+
"<#{policy}##{rule}: #{@value}>"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionPolicy
|
4
|
+
module Policy
|
5
|
+
# Adds callback-style checks to policies to
|
6
|
+
# extract common checks from rules.
|
7
|
+
#
|
8
|
+
# class ApplicationPolicy < ActionPolicy::Base
|
9
|
+
# authorize :user
|
10
|
+
# pre_check :allow_admins
|
11
|
+
#
|
12
|
+
# private
|
13
|
+
# # Allow every action for admins
|
14
|
+
# def allow_admins
|
15
|
+
# allow! if user.admin?
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# You can specify conditional pre-checks (through `except` / `only`) options
|
20
|
+
# and skip already defined pre-checks if necessary.
|
21
|
+
#
|
22
|
+
# class UserPolicy < ApplicationPolicy
|
23
|
+
# skip_pre_check :allow_admins, only: :destroy?
|
24
|
+
#
|
25
|
+
# def destroy?
|
26
|
+
# user.admin? && !record.admin?
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
module PreCheck
|
30
|
+
# Single pre-check instance.
|
31
|
+
#
|
32
|
+
# Implements filtering logic.
|
33
|
+
class Check
|
34
|
+
attr_reader :name, :policy_class
|
35
|
+
|
36
|
+
def initialize(policy, name, except: nil, only: nil)
|
37
|
+
if !except.nil? && !only.nil?
|
38
|
+
raise ArgumentError,
|
39
|
+
"Only one of `except` and `only` may be specified for pre-check"
|
40
|
+
end
|
41
|
+
|
42
|
+
@policy_class = policy
|
43
|
+
@name = name
|
44
|
+
@blacklist = Array(except) unless except.nil?
|
45
|
+
@whitelist = Array(only) unless only.nil?
|
46
|
+
|
47
|
+
rebuild_filter
|
48
|
+
end
|
49
|
+
|
50
|
+
def applicable?(rule)
|
51
|
+
return true if filter.nil?
|
52
|
+
filter.call(rule)
|
53
|
+
end
|
54
|
+
|
55
|
+
def call(policy) ; policy.send(name); end
|
56
|
+
|
57
|
+
def skip!(except: nil, only: nil)
|
58
|
+
if !except.nil? && !only.nil?
|
59
|
+
raise ArgumentError,
|
60
|
+
"Only one of `except` and `only` may be specified when skipping pre-check"
|
61
|
+
end
|
62
|
+
|
63
|
+
if except.nil? && only.nil?
|
64
|
+
raise ArgumentError,
|
65
|
+
"At least one of `except` and `only` must be specified when skipping pre-check"
|
66
|
+
end
|
67
|
+
|
68
|
+
if except
|
69
|
+
@whitelist = Array(except)
|
70
|
+
@whitelist -= blacklist if blacklist
|
71
|
+
@blacklist = nil
|
72
|
+
else
|
73
|
+
# only
|
74
|
+
@blacklist += Array(only) if blacklist
|
75
|
+
@whitelist -= Array(only) if whitelist
|
76
|
+
@blacklist = Array(only) if filter.nil?
|
77
|
+
end
|
78
|
+
|
79
|
+
rebuild_filter
|
80
|
+
end
|
81
|
+
# rubocop: enable
|
82
|
+
# rubocop: enable
|
83
|
+
|
84
|
+
def dup
|
85
|
+
self.class.new(
|
86
|
+
policy_class,
|
87
|
+
name,
|
88
|
+
except: blacklist&.dup,
|
89
|
+
only: whitelist&.dup
|
90
|
+
)
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
attr_reader :whitelist, :blacklist, :filter
|
96
|
+
|
97
|
+
def rebuild_filter
|
98
|
+
@filter =
|
99
|
+
if whitelist
|
100
|
+
proc { |rule| whitelist.include?(rule) }
|
101
|
+
elsif blacklist
|
102
|
+
proc { |rule| !blacklist.include?(rule) }
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
class << self
|
108
|
+
def included(base)
|
109
|
+
base.extend ClassMethods
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def run_pre_checks(rule)
|
114
|
+
self.class.pre_checks.each do |check|
|
115
|
+
next unless check.applicable?(rule)
|
116
|
+
check.call(self)
|
117
|
+
end
|
118
|
+
|
119
|
+
yield if block_given?
|
120
|
+
end
|
121
|
+
|
122
|
+
def __apply__(rule)
|
123
|
+
run_pre_checks(rule) { super }
|
124
|
+
end
|
125
|
+
|
126
|
+
module ClassMethods # :nodoc:
|
127
|
+
def pre_check(*names, **options)
|
128
|
+
names.each do |name|
|
129
|
+
# do not allow pre-check override
|
130
|
+
check = pre_checks.find { _1.name == name }
|
131
|
+
raise "Pre-check already defined: #{name}" unless check.nil?
|
132
|
+
|
133
|
+
pre_checks << Check.new(self, name, **options)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def skip_pre_check(*names, **options)
|
138
|
+
names.each do |name|
|
139
|
+
check = pre_checks.find { _1.name == name }
|
140
|
+
raise "Pre-check not found: #{name}" if check.nil?
|
141
|
+
|
142
|
+
# when no options provided we remove this check completely
|
143
|
+
next pre_checks.delete(check) if options.empty?
|
144
|
+
|
145
|
+
# otherwise duplicate and apply skip options
|
146
|
+
pre_checks[pre_checks.index(check)] = check.dup.tap { _1.skip!(**options) }
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def pre_checks
|
151
|
+
return @pre_checks if instance_variable_defined?(:@pre_checks)
|
152
|
+
|
153
|
+
@pre_checks = if superclass.respond_to?(:pre_checks)
|
154
|
+
superclass.pre_checks.dup
|
155
|
+
else
|
156
|
+
[]
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,210 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionPolicy
|
4
|
+
using RubyNext
|
5
|
+
|
6
|
+
module Policy
|
7
|
+
# Failures reasons store
|
8
|
+
class FailureReasons
|
9
|
+
attr_reader :reasons
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@reasons = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def add(policy_or_class, rule, details = nil)
|
16
|
+
policy_class = policy_or_class.is_a?(Module) ? policy_or_class : policy_or_class.class
|
17
|
+
reasons[policy_class] ||= []
|
18
|
+
|
19
|
+
if details.nil?
|
20
|
+
add_non_detailed_reason reasons[policy_class], rule
|
21
|
+
else
|
22
|
+
add_detailed_reason reasons[policy_class], with_details(rule, details)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Return Hash of the form:
|
27
|
+
# { policy_identifier => [rules, ...] }
|
28
|
+
def details() ; reasons.transform_keys(&:identifier); end
|
29
|
+
|
30
|
+
def empty?() ; reasons.empty?; end
|
31
|
+
|
32
|
+
def present?() ; !empty?; end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def add_non_detailed_reason(store, rule)
|
37
|
+
index =
|
38
|
+
if store.last.is_a?(::Hash)
|
39
|
+
store.size - 1
|
40
|
+
else
|
41
|
+
store.size
|
42
|
+
end
|
43
|
+
|
44
|
+
store.insert(index, rule)
|
45
|
+
end
|
46
|
+
|
47
|
+
def add_detailed_reason(store, detailed_rule)
|
48
|
+
store.last.is_a?(::Hash) || store << {}
|
49
|
+
store.last.merge!(detailed_rule)
|
50
|
+
end
|
51
|
+
|
52
|
+
def with_details(rule, details)
|
53
|
+
return rule if details.nil?
|
54
|
+
|
55
|
+
{rule => details}
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Extend ExecutionResult with `reasons` method
|
60
|
+
module ResultFailureReasons
|
61
|
+
def reasons
|
62
|
+
@reasons ||= FailureReasons.new
|
63
|
+
end
|
64
|
+
|
65
|
+
attr_accessor :details
|
66
|
+
|
67
|
+
def clear_details
|
68
|
+
@details = nil
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns all the details merged together
|
72
|
+
def all_details
|
73
|
+
return @all_details if defined?(@all_details)
|
74
|
+
|
75
|
+
@all_details = {}.tap do |all|
|
76
|
+
next unless defined?(@reasons)
|
77
|
+
|
78
|
+
reasons.reasons.each_value do |rules|
|
79
|
+
detailed_reasons = rules.last
|
80
|
+
|
81
|
+
next unless detailed_reasons.is_a?(Hash)
|
82
|
+
|
83
|
+
detailed_reasons.each_value do |details|
|
84
|
+
all.merge!(details)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Add reasons to inspect
|
91
|
+
def inspect
|
92
|
+
super.then do |str|
|
93
|
+
next str if reasons.empty?
|
94
|
+
str.sub(/>$/, " (reasons: #{reasons.details})")
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Provides failure reasons tracking functionality.
|
100
|
+
# That allows you to distinguish between the reasons why authorization was rejected.
|
101
|
+
#
|
102
|
+
# It's helpful when you compose policies (i.e. use one policy within another).
|
103
|
+
#
|
104
|
+
# For example:
|
105
|
+
#
|
106
|
+
# class ApplicantPolicy < ApplicationPolicy
|
107
|
+
# def show?
|
108
|
+
# user.has_permission?(:view_applicants) &&
|
109
|
+
# allowed_to?(:show?, object.stage)
|
110
|
+
# end
|
111
|
+
# end
|
112
|
+
#
|
113
|
+
# Now when you receive an exception, you have a reasons object, which contains additional
|
114
|
+
# information about the failure:
|
115
|
+
#
|
116
|
+
# rescue_from ActionPolicy::Unauthorized do |ex|
|
117
|
+
# ex.policy #=> ApplicantPolicy
|
118
|
+
# ex.rule #=> :show?
|
119
|
+
# ex.result.reasons.details #=> {stage: [:show?]}
|
120
|
+
# end
|
121
|
+
#
|
122
|
+
# NOTE: the reason key (`stage`) is a policy identifier (underscored class name by default).
|
123
|
+
# For namespaced policies it has a form of:
|
124
|
+
#
|
125
|
+
# class Admin::UserPolicy < ApplicationPolicy
|
126
|
+
# # ..
|
127
|
+
# end
|
128
|
+
#
|
129
|
+
# reasons.details #=> {:"admin/user" => [:show?]}
|
130
|
+
#
|
131
|
+
#
|
132
|
+
# You can also wrap _local_ rules into `allowed_to?` to populate reasons:
|
133
|
+
#
|
134
|
+
# class ApplicantPolicy < ApplicationPolicy
|
135
|
+
# def show?
|
136
|
+
# allowed_to?(:view_applicants?) &&
|
137
|
+
# allowed_to?(:show?, object.stage)
|
138
|
+
# end
|
139
|
+
#
|
140
|
+
# def view_applicants?
|
141
|
+
# user.has_permission?(:view_applicants)
|
142
|
+
# end
|
143
|
+
# end
|
144
|
+
#
|
145
|
+
# NOTE: there is `check?` alias for `allowed_to?`.
|
146
|
+
#
|
147
|
+
# You can provide additional details to your failure reasons by using
|
148
|
+
# a `details: { ... }` option:
|
149
|
+
#
|
150
|
+
# class ApplicantPolicy < ApplicationPolicy
|
151
|
+
# def show?
|
152
|
+
# allowed_to?(:show?, object.stage)
|
153
|
+
# end
|
154
|
+
# end
|
155
|
+
#
|
156
|
+
# class StagePolicy < ApplicationPolicy
|
157
|
+
# def show?
|
158
|
+
# # Add stage title to the failure reason (if any)
|
159
|
+
# # (could be used by client to show more descriptive message)
|
160
|
+
# details[:title] = record.title
|
161
|
+
#
|
162
|
+
# # then perform the checks
|
163
|
+
# user.stages.where(id: record.id).exists?
|
164
|
+
# end
|
165
|
+
# end
|
166
|
+
#
|
167
|
+
# # when accessing the reasons
|
168
|
+
# p ex.result.reasons.details #=> { stage: [{show?: {title: "Onboarding"}] }
|
169
|
+
#
|
170
|
+
# NOTE: when using detailed reasons, the `details` array contains as the last element
|
171
|
+
# a hash with ALL details reasons for the policy (in a form of <rule> => <details>).
|
172
|
+
#
|
173
|
+
module Reasons
|
174
|
+
class << self
|
175
|
+
def included(base)
|
176
|
+
base.result_class.prepend(ResultFailureReasons)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# Add additional details to the failure reason
|
181
|
+
def details
|
182
|
+
result.details ||= {}
|
183
|
+
end
|
184
|
+
|
185
|
+
def allowed_to?(rule, record = :__undef__, **options)
|
186
|
+
res =
|
187
|
+
if (record == :__undef__ || record == self.record) && options.empty?
|
188
|
+
policy = self
|
189
|
+
with_clean_result { apply(rule) }
|
190
|
+
else
|
191
|
+
policy = policy_for(record: record, **options)
|
192
|
+
|
193
|
+
policy.apply(rule)
|
194
|
+
policy.result
|
195
|
+
end
|
196
|
+
|
197
|
+
result&.reasons&.add(policy, rule, res.details) if res.fail?
|
198
|
+
|
199
|
+
res.clear_details
|
200
|
+
|
201
|
+
res.success?
|
202
|
+
end
|
203
|
+
|
204
|
+
def deny!(reason = nil)
|
205
|
+
result&.reasons&.add(self, reason) if reason
|
206
|
+
super()
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action_policy/behaviours/scoping"
|
4
|
+
|
5
|
+
require "action_policy/utils/suggest_message"
|
6
|
+
|
7
|
+
module ActionPolicy
|
8
|
+
class UnknownScopeType < Error # :nodoc:
|
9
|
+
include ActionPolicy::SuggestMessage
|
10
|
+
|
11
|
+
MESSAGE_TEMPLATE = "Unknown policy scope type :%s for %s%s"
|
12
|
+
|
13
|
+
attr_reader :message
|
14
|
+
|
15
|
+
def initialize(policy_class, type)
|
16
|
+
@message = format(
|
17
|
+
MESSAGE_TEMPLATE,
|
18
|
+
type, policy_class,
|
19
|
+
suggest(type, policy_class.scoping_handlers.keys)
|
20
|
+
)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class UnknownNamedScope < Error # :nodoc:
|
25
|
+
include ActionPolicy::SuggestMessage
|
26
|
+
|
27
|
+
MESSAGE_TEMPLATE = "Unknown named scope :%s for type :%s for %s%s"
|
28
|
+
|
29
|
+
attr_reader :message
|
30
|
+
|
31
|
+
def initialize(policy_class, type, name)
|
32
|
+
@message = format(
|
33
|
+
MESSAGE_TEMPLATE, name, type, policy_class,
|
34
|
+
suggest(name, policy_class.scoping_handlers[type].keys)
|
35
|
+
)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class UnrecognizedScopeTarget < Error # :nodoc:
|
40
|
+
MESSAGE_TEMPLATE = "Couldn't infer scope type for %s instance"
|
41
|
+
|
42
|
+
attr_reader :message
|
43
|
+
|
44
|
+
def initialize(target)
|
45
|
+
target_class = target.is_a?(Module) ? target : target.class
|
46
|
+
|
47
|
+
@message = format(
|
48
|
+
MESSAGE_TEMPLATE, target_class
|
49
|
+
)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
module Policy
|
54
|
+
# Scoping is used to modify the _object under authorization_.
|
55
|
+
#
|
56
|
+
# The most common situation is when you want to _scope_ the collection depending
|
57
|
+
# on the current user permissions.
|
58
|
+
#
|
59
|
+
# For example:
|
60
|
+
#
|
61
|
+
# class ApplicationPolicy < ActionPolicy::Base
|
62
|
+
# # Scoping only makes sense when you have the authorization context
|
63
|
+
# authorize :user
|
64
|
+
#
|
65
|
+
# # :relation here is a scoping type
|
66
|
+
# scope_for :relation do |relation|
|
67
|
+
# # authorization context is available within a scope
|
68
|
+
# if user.admin?
|
69
|
+
# relation
|
70
|
+
# else
|
71
|
+
# relation.publicly_visible
|
72
|
+
# end
|
73
|
+
# end
|
74
|
+
# end
|
75
|
+
#
|
76
|
+
# base_scope = User.all
|
77
|
+
# authorized_scope = ApplicantPolicy.new(user: user)
|
78
|
+
# .apply_scope(base_scope, type: :relation)
|
79
|
+
module Scoping
|
80
|
+
class << self
|
81
|
+
def included(base)
|
82
|
+
base.extend ClassMethods
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
include ActionPolicy::Behaviours::Scoping
|
87
|
+
|
88
|
+
# Pass target to the scope handler of the specified type and name.
|
89
|
+
# If `name` is not specified then `:default` name is used.
|
90
|
+
# If `type` is not specified then we try to infer the type from the
|
91
|
+
# target class.
|
92
|
+
def apply_scope(target, type:, name: :default, scope_options: nil)
|
93
|
+
raise ActionPolicy::UnknownScopeType.new(self.class, type) unless
|
94
|
+
self.class.scoping_handlers.key?(type)
|
95
|
+
|
96
|
+
raise ActionPolicy::UnknownNamedScope.new(self.class, type, name) unless
|
97
|
+
self.class.scoping_handlers[type].key?(name)
|
98
|
+
|
99
|
+
mid = :"__scoping__#{type}__#{name}"
|
100
|
+
scope_options ? send(mid, target, **scope_options) : send(mid, target)
|
101
|
+
end
|
102
|
+
|
103
|
+
def resolve_scope_type(target)
|
104
|
+
lookup_type_from_target(target) ||
|
105
|
+
raise(ActionPolicy::UnrecognizedScopeTarget, target)
|
106
|
+
end
|
107
|
+
|
108
|
+
def lookup_type_from_target(target)
|
109
|
+
self.class.scope_matchers.detect do |(_type, matcher)|
|
110
|
+
matcher === target
|
111
|
+
end&.first
|
112
|
+
end
|
113
|
+
|
114
|
+
module ClassMethods # :nodoc:
|
115
|
+
# Register a new scoping method for the `type`
|
116
|
+
def scope_for(type, name = :default, &block)
|
117
|
+
mid = :"__scoping__#{type}__#{name}"
|
118
|
+
|
119
|
+
define_method(mid, &block)
|
120
|
+
|
121
|
+
scoping_handlers[type][name] = mid
|
122
|
+
end
|
123
|
+
|
124
|
+
def scoping_handlers
|
125
|
+
return @scoping_handlers if instance_variable_defined?(:@scoping_handlers)
|
126
|
+
|
127
|
+
@scoping_handlers =
|
128
|
+
Hash.new { |h, k| h[k] = {} }.tap do |handlers|
|
129
|
+
if superclass.respond_to?(:scoping_handlers)
|
130
|
+
superclass.scoping_handlers.each do |k, v|
|
131
|
+
handlers[k] = v.dup
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Define scope type matcher.
|
138
|
+
#
|
139
|
+
# Scope matcher is an object that implements `#===` (_case equality_) or a Proc.
|
140
|
+
#
|
141
|
+
# When no type is provided when applying a scope we try to infer a type
|
142
|
+
# from the target object by calling matchers one by one until we find a matching
|
143
|
+
# type (i.e. there is a matcher which returns `true` when applying it to the target).
|
144
|
+
def scope_matcher(type, class_or_proc)
|
145
|
+
scope_matchers << [type, class_or_proc]
|
146
|
+
end
|
147
|
+
|
148
|
+
def scope_matchers
|
149
|
+
return @scope_matchers if instance_variable_defined?(:@scope_matchers)
|
150
|
+
|
151
|
+
@scope_matchers = if superclass.respond_to?(:scope_matchers)
|
152
|
+
superclass.scope_matchers.dup
|
153
|
+
else
|
154
|
+
[]
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|