action_policy 0.7.4 → 0.7.6

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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -3
  3. data/lib/.rbnext/3.0/action_policy/behaviours/policy_for.rb +1 -1
  4. data/lib/.rbnext/3.0/action_policy/policy/cache.rb +1 -1
  5. data/lib/.rbnext/3.0/action_policy/policy/execution_result.rb +1 -1
  6. data/lib/.rbnext/3.0/action_policy/policy/pre_check.rb +3 -3
  7. data/lib/.rbnext/3.0/action_policy/rspec/be_authorized_to.rb +4 -4
  8. data/lib/.rbnext/3.0/action_policy/rspec/have_authorized_scope.rb +4 -4
  9. data/lib/.rbnext/3.0/action_policy/utils/pretty_print.rb +2 -2
  10. data/lib/.rbnext/3.1/action_policy/behaviours/policy_for.rb +1 -1
  11. data/lib/.rbnext/3.2/action_policy/behaviours/policy_for.rb +1 -1
  12. data/lib/.rbnext/3.2/action_policy/rspec/be_authorized_to.rb +4 -4
  13. data/lib/.rbnext/3.2/action_policy/rspec/have_authorized_scope.rb +4 -4
  14. data/lib/.rbnext/3.4/action_policy/behaviours/policy_for.rb +70 -0
  15. data/lib/.rbnext/3.4/action_policy/i18n.rb +56 -0
  16. data/lib/.rbnext/3.4/action_policy/policy/cache.rb +101 -0
  17. data/lib/.rbnext/3.4/action_policy/policy/pre_check.rb +160 -0
  18. data/lib/.rbnext/3.4/action_policy/rspec/be_authorized_to.rb +96 -0
  19. data/lib/.rbnext/3.4/action_policy/rspec/have_authorized_scope.rb +130 -0
  20. data/lib/.rbnext/3.4/action_policy/utils/pretty_print.rb +155 -0
  21. data/lib/action_policy/behaviour.rb +1 -1
  22. data/lib/action_policy/behaviours/policy_for.rb +1 -1
  23. data/lib/action_policy/i18n.rb +1 -1
  24. data/lib/action_policy/policy/cache.rb +1 -1
  25. data/lib/action_policy/policy/execution_result.rb +1 -1
  26. data/lib/action_policy/policy/pre_check.rb +3 -3
  27. data/lib/action_policy/rails/controller.rb +45 -4
  28. data/lib/action_policy/rails/policy/instrumentation.rb +1 -0
  29. data/lib/action_policy/rspec/be_authorized_to.rb +3 -3
  30. data/lib/action_policy/rspec/have_authorized_scope.rb +3 -3
  31. data/lib/action_policy/test_helper.rb +1 -1
  32. data/lib/action_policy/utils/pretty_print.rb +2 -2
  33. data/lib/action_policy/version.rb +1 -1
  34. data/lib/action_policy.rb +1 -1
  35. data/lib/ruby_lsp/action_policy/addon.rb +170 -0
  36. metadata +11 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c2ca6d3ab4293001cc4b07e7fe5503a8c06a8b72684c0f7a9c4e7a4ef8a32c89
4
- data.tar.gz: f1e726704611a3bc9ade5d3466fd64c4a5c9faddf5aa8338f3179ec4da2381e5
3
+ metadata.gz: 322dfe78ddec91cb0d7c46b290f69484ba2fdf417c85c687d23e6b521db3e097
4
+ data.tar.gz: b263d5a7000f202b480331e4dd486726563ab83a4828bd57dbaa8c2e8a3b86a3
5
5
  SHA512:
6
- metadata.gz: aa35a3efefa37fc64066a78899b9b03a2283e2458471425a2e7cc2bc0b4ff829e87beda744057866f661e37fc54f14d094ba0fc446ea155537042478109576a7
7
- data.tar.gz: d90069b7a1b3bea22c2d5e6dd990231f1bf24f85fd50081a92a54bcd5c271faa42f40c9c526a8434abd793e6f4c928b5cb28d4dad3fbaeb2bf3701f67fd4fd21
6
+ metadata.gz: 93628bb047eb417395cc9b7f0b15f2f97882d327c32212105d186ef42187fc24e84abd37bdf40968ae5a5988d318d717f0fb3af454f5fb14059a3dc84fe98c08
7
+ data.tar.gz: 9d0387f6aea496ac9d3affdb6cfe89b9fef008d9ff2f96a97a69ce19c0832f8fe01d2023f4497a150325e00e341d6a170b8218e3ce47e06a96d8de28aea38cb5
data/CHANGELOG.md CHANGED
@@ -2,13 +2,23 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 0.7.6 (2025-01-13)
6
+
7
+ - Execute proc passed to the `:through` option of `authorize` against `self` ([@Pagehey][])
8
+
9
+ - Add authorization result to the instrumentation payload (as `event[:result]`). ([@palkan][])
10
+
11
+ ## 0.7.5 (2025-05-09) 🎇
12
+
13
+ - Ensure `result.value` is true or false. ([@palkan][])
14
+
5
15
  ## 0.7.4 (2025-03-12)
6
16
 
7
- - Let authorize! return the policy record ([@sedubois][])
17
+ - Let authorize! return the policy record. ([@sedubois][])
8
18
 
9
- - Enable `allowance_to` as a helper method by default ([@stephannv][])
19
+ - Enable `allowance_to` as a helper method by default. ([@stephannv][])
10
20
 
11
- - Allow the `:through` option of `authorize` to be passed a proc ([@brendon][])
21
+ - Allow the `:through` option of `authorize` to be passed a Proc. ([@brendon][])
12
22
 
13
23
  ## 0.7.3 (2024-12-18)
14
24
 
@@ -549,3 +559,4 @@ This value is now stored in a cache (if any) instead of just the call result (`t
549
559
  [@Spone]: https://github.com/Spone
550
560
  [@stephannv]: https://github.com/stephannv
551
561
  [@sedubois]: https://github.com/sedubois
562
+ [@Pagehey]: https://github.com/Pagehey
@@ -61,7 +61,7 @@ module ActionPolicy
61
61
 
62
62
  def policy_for_cache_key(record:, with: nil, namespace: nil, context: authorization_context, **__kwrest__)
63
63
  record_key = record._policy_cache_key(use_object_id: true)
64
- context_key = context.values.map { _1._policy_cache_key(use_object_id: true) }.join(".")
64
+ context_key = context.values.map { |it| it._policy_cache_key(use_object_id: true) }.join(".")
65
65
 
66
66
  "#{namespace}/#{with}/#{context_key}/#{record_key}"
67
67
  end
@@ -29,7 +29,7 @@ module ActionPolicy # :nodoc:
29
29
  [
30
30
  cache_namespace,
31
31
  *parts
32
- ].map { _1._policy_cache_key }.join("/")
32
+ ].map { |it| it._policy_cache_key }.join("/")
33
33
  end
34
34
 
35
35
  def rule_cache_key(rule)
@@ -16,7 +16,7 @@ module ActionPolicy
16
16
 
17
17
  # Populate the final value
18
18
  def load(value)
19
- @value = value
19
+ @value = !!value
20
20
  end
21
21
 
22
22
  def success?() ; @value == true; end
@@ -125,7 +125,7 @@ module ActionPolicy
125
125
  def pre_check(*names, **options)
126
126
  names.each do |name|
127
127
  # do not allow pre-check override
128
- check = pre_checks.find { _1.name == name }
128
+ check = pre_checks.find { |it| it.name == name }
129
129
  raise "Pre-check already defined: #{name}" unless check.nil?
130
130
 
131
131
  pre_checks << Check.new(self, name, **options)
@@ -134,14 +134,14 @@ module ActionPolicy
134
134
 
135
135
  def skip_pre_check(*names, **options)
136
136
  names.each do |name|
137
- check = pre_checks.find { _1.name == name }
137
+ check = pre_checks.find { |it| it.name == name }
138
138
  raise "Pre-check not found: #{name}" if check.nil?
139
139
 
140
140
  # when no options provided we remove this check completely
141
141
  next pre_checks.delete(check) if options.empty?
142
142
 
143
143
  # otherwise duplicate and apply skip options
144
- pre_checks[pre_checks.index(check)] = check.dup.tap { _1.skip!(**options) }
144
+ pre_checks[pre_checks.index(check)] = check.dup.tap { |it| it.skip!(**options) }
145
145
  end
146
146
  end
147
147
 
@@ -51,7 +51,7 @@ module ActionPolicy
51
51
 
52
52
  @actual_calls = ActionPolicy::Testing::AuthorizeTracker.calls
53
53
 
54
- actual_calls.any? { _1.matches?(policy, rule, target, context) }
54
+ actual_calls.any? { |it| it.matches?(policy, rule, target, context) }
55
55
  end
56
56
 
57
57
  def does_not_match?(*__rest__)
@@ -63,7 +63,7 @@ module ActionPolicy
63
63
  def failure_message
64
64
  "expected #{formatted_record} " \
65
65
  "to be authorized with #{policy}##{rule}, " \
66
- "#{context ? "and context #{context.inspect}, " : ""}" \
66
+ "#{"and context #{context.inspect}, " if context}" \
67
67
  "but #{actual_calls_message}"
68
68
  end
69
69
 
@@ -77,8 +77,8 @@ module ActionPolicy
77
77
  end
78
78
 
79
79
  def formatted_calls
80
- actual_calls.map do
81
- " - #{_1.inspect}"
80
+ actual_calls.map do |it|
81
+ " - #{it.inspect}"
82
82
  end.join("\n")
83
83
  end
84
84
 
@@ -61,7 +61,7 @@ module ActionPolicy
61
61
 
62
62
  @actual_scopes = ActionPolicy::Testing::AuthorizeTracker.scopings
63
63
 
64
- matching_scopes = actual_scopes.select { _1.matches?(policy, type, name, scope_options, context) }
64
+ matching_scopes = actual_scopes.select { |it| it.matches?(policy, type, name, scope_options, context) }
65
65
 
66
66
  return false if matching_scopes.empty?
67
67
 
@@ -85,7 +85,7 @@ module ActionPolicy
85
85
  def failure_message
86
86
  "expected a scoping named :#{name} for type :#{type} " \
87
87
  "#{scope_options_message} " \
88
- "#{context ? "and context #{context.inspect} " : ""}" \
88
+ "#{"and context #{context.inspect} " if context}" \
89
89
  "from #{policy} to have been applied, " \
90
90
  "but #{actual_scopes_message}"
91
91
  end
@@ -113,8 +113,8 @@ module ActionPolicy
113
113
  end
114
114
 
115
115
  def formatted_scopings
116
- actual_scopes.map do
117
- " - #{_1.inspect}"
116
+ actual_scopes.map do |it|
117
+ " - #{it.inspect}"
118
118
  end.join("\n")
119
119
  end
120
120
  end
@@ -111,7 +111,7 @@ module ActionPolicy
111
111
  end
112
112
 
113
113
  def indented(str)
114
- "#{indent.zero? ? "↳ " : ""}#{" " * indent}#{str}".tap do
114
+ "#{"↳ " if indent.zero?}#{" " * indent}#{str}".tap do
115
115
  # increase indent after the first expression
116
116
  self.indent += 2 if indent.zero?
117
117
  end
@@ -119,7 +119,7 @@ module ActionPolicy
119
119
 
120
120
  # Some lines should not be evaled
121
121
  def ignore_exp?(exp)
122
- PrettyPrint.ignore_expressions.any? { exp.match?(_1) }
122
+ PrettyPrint.ignore_expressions.any? { |it| exp.match?(it) }
123
123
  end
124
124
  end
125
125
 
@@ -61,7 +61,7 @@ module ActionPolicy
61
61
 
62
62
  def policy_for_cache_key(record:, with: nil, namespace: nil, context: authorization_context, **__kwrest__)
63
63
  record_key = record._policy_cache_key(use_object_id: true)
64
- context_key = context.values.map { _1._policy_cache_key(use_object_id: true) }.join(".")
64
+ context_key = context.values.map { |it| it._policy_cache_key(use_object_id: true) }.join(".")
65
65
 
66
66
  "#{namespace}/#{with}/#{context_key}/#{record_key}"
67
67
  end
@@ -61,7 +61,7 @@ module ActionPolicy
61
61
 
62
62
  def policy_for_cache_key(record:, with: nil, namespace: nil, context: authorization_context, **__kwrest__)
63
63
  record_key = record._policy_cache_key(use_object_id: true)
64
- context_key = context.values.map { _1._policy_cache_key(use_object_id: true) }.join(".")
64
+ context_key = context.values.map { |it| it._policy_cache_key(use_object_id: true) }.join(".")
65
65
 
66
66
  "#{namespace}/#{with}/#{context_key}/#{record_key}"
67
67
  end
@@ -51,7 +51,7 @@ module ActionPolicy
51
51
 
52
52
  @actual_calls = ActionPolicy::Testing::AuthorizeTracker.calls
53
53
 
54
- actual_calls.any? { _1.matches?(policy, rule, target, context) }
54
+ actual_calls.any? { |it| it.matches?(policy, rule, target, context) }
55
55
  end
56
56
 
57
57
  def does_not_match?(*__rest__)
@@ -63,7 +63,7 @@ module ActionPolicy
63
63
  def failure_message
64
64
  "expected #{formatted_record} " \
65
65
  "to be authorized with #{policy}##{rule}, " \
66
- "#{context ? "and context #{context.inspect}, " : ""}" \
66
+ "#{"and context #{context.inspect}, " if context}" \
67
67
  "but #{actual_calls_message}"
68
68
  end
69
69
 
@@ -77,8 +77,8 @@ module ActionPolicy
77
77
  end
78
78
 
79
79
  def formatted_calls
80
- actual_calls.map do
81
- " - #{_1.inspect}"
80
+ actual_calls.map do |it|
81
+ " - #{it.inspect}"
82
82
  end.join("\n")
83
83
  end
84
84
 
@@ -61,7 +61,7 @@ module ActionPolicy
61
61
 
62
62
  @actual_scopes = ActionPolicy::Testing::AuthorizeTracker.scopings
63
63
 
64
- matching_scopes = actual_scopes.select { _1.matches?(policy, type, name, scope_options, context) }
64
+ matching_scopes = actual_scopes.select { |it| it.matches?(policy, type, name, scope_options, context) }
65
65
 
66
66
  return false if matching_scopes.empty?
67
67
 
@@ -85,7 +85,7 @@ module ActionPolicy
85
85
  def failure_message
86
86
  "expected a scoping named :#{name} for type :#{type} " \
87
87
  "#{scope_options_message} " \
88
- "#{context ? "and context #{context.inspect} " : ""}" \
88
+ "#{"and context #{context.inspect} " if context}" \
89
89
  "from #{policy} to have been applied, " \
90
90
  "but #{actual_scopes_message}"
91
91
  end
@@ -113,8 +113,8 @@ module ActionPolicy
113
113
  end
114
114
 
115
115
  def formatted_scopings
116
- actual_scopes.map do
117
- " - #{_1.inspect}"
116
+ actual_scopes.map do |it|
117
+ " - #{it.inspect}"
118
118
  end.join("\n")
119
119
  end
120
120
  end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionPolicy
4
+ module Behaviours
5
+ # Adds `policy_for` method
6
+ module PolicyFor
7
+ require "action_policy/ext/policy_cache_key"
8
+ using ActionPolicy::Ext::PolicyCacheKey
9
+
10
+ # Returns policy instance for the record.
11
+ def policy_for(record:, with: nil, namespace: authorization_namespace, context: nil, allow_nil: false, default: default_authorization_policy_class, strict_namespace: authorization_strict_namespace)
12
+ context = context ? build_authorization_context.merge(context) : authorization_context
13
+
14
+ policy_class = with || ::ActionPolicy.lookup(
15
+ record,
16
+ namespace:, context:, allow_nil:, default:, strict_namespace:
17
+ )
18
+ policy_class&.new(record, **context)
19
+ end
20
+
21
+ def authorization_context = @authorization_context ||= build_authorization_context
22
+
23
+ def build_authorization_context
24
+ Kernel.raise NotImplementedError, "Please, define `build_authorization_context` method!"
25
+ end
26
+
27
+ def authorization_namespace
28
+ # override to provide specific authorization namespace
29
+ end
30
+
31
+ def default_authorization_policy_class
32
+ # override to provide a policy class use when no policy found
33
+ end
34
+
35
+ def authorization_strict_namespace
36
+ # override to provide strict namespace lookup option
37
+ end
38
+
39
+ # Override this method to provide implicit authorization target
40
+ # that would be used in case `record` is not specified in
41
+ # `authorize!` and `allowed_to?` call.
42
+ #
43
+ # It is also used to infer a policy for scoping (in `authorized_scope` method).
44
+ def implicit_authorization_target
45
+ # no-op
46
+ end
47
+
48
+ # Return implicit authorization target or raises an exception if it's nil
49
+ def implicit_authorization_target!
50
+ implicit_authorization_target || Kernel.raise(
51
+ NotFound,
52
+ [
53
+ self,
54
+ "Couldn't find implicit authorization target " \
55
+ "for #{self.class}. " \
56
+ "Please, provide policy class explicitly using `with` option or " \
57
+ "define the `implicit_authorization_target` method."
58
+ ]
59
+ )
60
+ end
61
+
62
+ def policy_for_cache_key(record:, with: nil, namespace: nil, context: authorization_context, **)
63
+ record_key = record._policy_cache_key(use_object_id: true)
64
+ context_key = context.values.map { |it| it._policy_cache_key(use_object_id: true) }.join(".")
65
+
66
+ "#{namespace}/#{with}/#{context_key}/#{record_key}"
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionPolicy
4
+ module I18n # :nodoc:
5
+ DEFAULT_UNAUTHORIZED_MESSAGE = "You are not authorized to perform this action"
6
+
7
+ class << self
8
+ def full_message(policy_class, rule, details = nil)
9
+ candidates = candidates_for(policy_class, rule)
10
+
11
+ options = {scope: :action_policy}
12
+ options.merge!(details) unless details.nil?
13
+
14
+ ::I18n.t(
15
+ candidates.shift,
16
+ default: candidates,
17
+ **options
18
+ )
19
+ end
20
+
21
+ private
22
+
23
+ def candidates_for(policy_class, rule)
24
+ policy_hierarchy = policy_class.ancestors.select { |it| it.respond_to?(:identifier) }
25
+ [
26
+ *policy_hierarchy.map { |klass| :"policy.#{klass.identifier}.#{rule}" },
27
+ :"policy.#{rule}",
28
+ :unauthorized,
29
+ DEFAULT_UNAUTHORIZED_MESSAGE
30
+ ]
31
+ end
32
+ end
33
+
34
+ ActionPolicy::Policy::FailureReasons.prepend(Module.new do
35
+ def full_messages
36
+ reasons.flat_map do |policy_klass, rules|
37
+ rules.flat_map do |rule|
38
+ if rule.is_a?(::Hash)
39
+ rule.map do |key, details|
40
+ ActionPolicy::I18n.full_message(policy_klass, key, details)
41
+ end
42
+ else
43
+ ActionPolicy::I18n.full_message(policy_klass, rule)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end)
49
+
50
+ ActionPolicy::Policy::ExecutionResult.prepend(Module.new do
51
+ def message
52
+ ActionPolicy::I18n.full_message(policy, rule, details)
53
+ end
54
+ end)
55
+ end
56
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_policy/version"
4
+
5
+ module ActionPolicy # :nodoc:
6
+ using RubyNext
7
+
8
+ # By default cache namespace (or prefix) contains major and minor version of the gem
9
+ CACHE_NAMESPACE = "acp:#{ActionPolicy::VERSION.split(".").take(2).join(".")}"
10
+
11
+ require "action_policy/ext/policy_cache_key"
12
+
13
+ using ActionPolicy::Ext::PolicyCacheKey
14
+
15
+ module Policy
16
+ # Provides long-lived cache through ActionPolicy.cache_store.
17
+ #
18
+ # NOTE: if cache_store is nil then we silently skip all the caching.
19
+ module Cache
20
+ class << self
21
+ def included(base)
22
+ base.extend ClassMethods
23
+ end
24
+ end
25
+
26
+ def cache_namespace() = ActionPolicy::CACHE_NAMESPACE
27
+
28
+ def cache_key(*parts)
29
+ [
30
+ cache_namespace,
31
+ *parts
32
+ ].map { |it| it._policy_cache_key }.join("/")
33
+ end
34
+
35
+ def rule_cache_key(rule)
36
+ cache_key(
37
+ context_cache_key,
38
+ record,
39
+ self.class,
40
+ rule
41
+ )
42
+ end
43
+
44
+ def context_cache_key
45
+ authorization_context.map { _2._policy_cache_key.to_s }.join("/")
46
+ end
47
+
48
+ def apply_with_cache(rule)
49
+ options = self.class.cached_rules.fetch(rule)
50
+ key = rule_cache_key(rule)
51
+
52
+ ActionPolicy.cache_store.then do |store|
53
+ result = store.read(key)
54
+ unless result.nil?
55
+ result.cached!
56
+ next result
57
+ end
58
+ yield.tap do |result|
59
+ store.write(key, result, options)
60
+ end
61
+ end
62
+ end
63
+
64
+ def apply_r(rule)
65
+ return super if ActionPolicy.cache_store.nil? ||
66
+ !self.class.cached_rules.key?(rule)
67
+
68
+ apply_with_cache(rule) { super }
69
+ end
70
+
71
+ def cache(*parts, **options)
72
+ key = cache_key(*parts)
73
+ ActionPolicy.cache_store.then do |store|
74
+ res = store.read(key)
75
+ next res unless res.nil?
76
+ res = yield
77
+ store.write(key, res, options)
78
+ res
79
+ end
80
+ end
81
+
82
+ module ClassMethods # :nodoc:
83
+ def cache(*rules, **options)
84
+ rules.each do |rule|
85
+ cached_rules[rule] = options
86
+ end
87
+ end
88
+
89
+ def cached_rules
90
+ return @cached_rules if instance_variable_defined?(:@cached_rules)
91
+
92
+ @cached_rules = if superclass.respond_to?(:cached_rules)
93
+ superclass.cached_rules.dup
94
+ else
95
+ {}
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,160 @@
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)
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
+
82
+ def dup
83
+ self.class.new(
84
+ policy_class,
85
+ name,
86
+ except: blacklist&.dup,
87
+ only: whitelist&.dup
88
+ )
89
+ end
90
+
91
+ private
92
+
93
+ attr_reader :whitelist, :blacklist, :filter
94
+
95
+ def rebuild_filter
96
+ @filter =
97
+ if whitelist
98
+ proc { |rule| whitelist.include?(rule) }
99
+ elsif blacklist
100
+ proc { |rule| !blacklist.include?(rule) }
101
+ end
102
+ end
103
+ end
104
+
105
+ class << self
106
+ def included(base)
107
+ base.extend ClassMethods
108
+ end
109
+ end
110
+
111
+ def run_pre_checks(rule)
112
+ self.class.pre_checks.each do |check|
113
+ next unless check.applicable?(rule)
114
+ check.call(self)
115
+ end
116
+
117
+ yield if block_given?
118
+ end
119
+
120
+ def __apply__(rule)
121
+ run_pre_checks(rule) { super }
122
+ end
123
+
124
+ module ClassMethods # :nodoc:
125
+ def pre_check(*names, **options)
126
+ names.each do |name|
127
+ # do not allow pre-check override
128
+ check = pre_checks.find { |it| it.name == name }
129
+ raise "Pre-check already defined: #{name}" unless check.nil?
130
+
131
+ pre_checks << Check.new(self, name, **options)
132
+ end
133
+ end
134
+
135
+ def skip_pre_check(*names, **options)
136
+ names.each do |name|
137
+ check = pre_checks.find { |it| it.name == name }
138
+ raise "Pre-check not found: #{name}" if check.nil?
139
+
140
+ # when no options provided we remove this check completely
141
+ next pre_checks.delete(check) if options.empty?
142
+
143
+ # otherwise duplicate and apply skip options
144
+ pre_checks[pre_checks.index(check)] = check.dup.tap { |it| it.skip!(**options) }
145
+ end
146
+ end
147
+
148
+ def pre_checks
149
+ return @pre_checks if instance_variable_defined?(:@pre_checks)
150
+
151
+ @pre_checks = if superclass.respond_to?(:pre_checks)
152
+ superclass.pre_checks.dup
153
+ else
154
+ []
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end