action_policy 0.1.4 → 0.2.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dd1a7f10e0934ff905e63da4fe68970165813f062ee10c2534eba30acbdcb04b
4
- data.tar.gz: 0e26b4d23cd11d96accad3c4ba15ff7dfbd68483b1592a98dff181afe6c699a3
3
+ metadata.gz: a261b126090b29222039294b84b4575fa796bf115033c2514fe301e0ec3d34af
4
+ data.tar.gz: 05f9d1cd84b2f4ce2b951267e0122fb54a436a5e51f0dd2f9a7d79e06f30d6ec
5
5
  SHA512:
6
- metadata.gz: b9643d2b9298058b34d42717b25cb258f53d8cd1aa4314889fab4c830fe781af82e3688400453c1edb0fd41273c5abcdf77f36031d6afc984ececb666570b4f0
7
- data.tar.gz: ea54f95f99eeb8e60f0554c9bc4b1ae1dccd235c34efba012c08e7a222f3ea1b6fcb873bc19fad1f147af526724e326ad913e6663a8e88b072b315f0e25e3ac4
6
+ metadata.gz: 80bbbe6ec61bf7241fe0e823844120292e6b8efd98a3c06834d98ac797cf92ebdffa21332e40305770622d5120d391443d0570193001a5e358f1ee6056c09bbb
7
+ data.tar.gz: 7f6990f7bcd5998eef536640a451591801afd073e29b25a764c500b1d0f535e3cfaf1a176f0adf930f498386ca9f206aa377b4c5adf1b2d119be642681fa7e81
data/.rubocop.yml CHANGED
@@ -39,6 +39,10 @@ Naming/FileName:
39
39
  - 'Gemfile'
40
40
  - '**/*.md'
41
41
 
42
+ Layout/InitialIndentation:
43
+ Exclude:
44
+ - 'CHANGELOG.md'
45
+
42
46
  Naming/UncommunicativeMethodParamName:
43
47
  Enabled: false
44
48
 
data/.travis.yml CHANGED
@@ -11,7 +11,7 @@ matrix:
11
11
  include:
12
12
  - rvm: ruby-head
13
13
  gemfile: gemfiles/railsmaster.gemfile
14
- - rvm: jruby-9.1.0.0
14
+ - rvm: jruby-9.2.0.0
15
15
  gemfile: gemfiles/jruby.gemfile
16
16
  - rvm: 2.5.1
17
17
  gemfile: Gemfile
@@ -24,5 +24,5 @@ matrix:
24
24
  allow_failures:
25
25
  - rvm: ruby-head
26
26
  gemfile: gemfiles/railsmaster.gemfile
27
- - rvm: jruby-9.1.0.0
27
+ - rvm: jruby-9.2.0.0
28
28
  gemfile: gemfiles/jruby.gemfile
data/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  ## master
2
2
 
3
+ ## 0.2.0 (2018-06-17)
4
+
5
+ - Make `action_policy` JRuby-compatible. ([@palkan][])
6
+
7
+ - Add `reasons.details`. ([@palkan][])
8
+
9
+ ```ruby
10
+ rescue_from ActionPolicy::Unauthorized do |ex|
11
+ ex.result.reasons.details #=> { stage: [:show?] }
12
+ end
13
+ ```
14
+
15
+ - Add `ExecutionResult`. ([@palkan][])
16
+
17
+ ExecutionResult contains all the rule application artifacts: the result (`true` / `false`),
18
+ failures reasons.
19
+
20
+ This value is now stored in a cache (if any) instead of just the call result (`true` / `false`).
21
+
22
+ - Add `Policy.identifier`. ([@palkan][])
23
+
3
24
  ## 0.1.4 (2018-06-06)
4
25
 
5
26
  - Fix Railtie injection hook. ([@palkan][])
data/docs/caching.md CHANGED
@@ -155,7 +155,9 @@ Rails.application.configure do |config|
155
155
  end
156
156
  ```
157
157
 
158
- Cache store must provide at least a `#fetch(key, **options, &block)` method.
158
+ Cache store must provide at least a `#read(key)` and `write(key, value, **options)` methods.
159
+
160
+ **NOTE:** cache store also should take care of serialiation/deserialization since the `value` is `ExecutionResult` instance (which contains also some additional information, e.g. failure reasons). Rails cache store supports serialization/deserialization out-of-the-box.
159
161
 
160
162
  By default, Action Policy builds a cache key using the following scheme:
161
163
 
@@ -10,10 +10,12 @@ It looks like this:
10
10
  class ActionPolicy::Base
11
11
  include ActionPolicy::Policy::Core
12
12
  include ActionPolicy::Policy::Authorization
13
- include ActionPolicy::Policy::Reasons
14
13
  include ActionPolicy::Policy::PreCheck
14
+ include ActionPolicy::Policy::Reasons
15
15
  include ActionPolicy::Policy::Aliases
16
+ include ActionPolicy::Policy::Cache
16
17
  include ActionPolicy::Policy::CachedApply
18
+ include ActionPolicy::Policy::Defaults
17
19
 
18
20
  # ActionPolicy::Policy::Defaults module adds the following
19
21
 
data/docs/quick_start.md CHANGED
@@ -74,7 +74,15 @@ end
74
74
 
75
75
  In the above case, Action Policy automatically infers a policy class and a rule to verify access: `@post -> Post -> PostPolicy`, rule is inferred from the action name (`update -> update?`), and `current_user` is used as `user` within the policy by default (read more about [authorization context](authorization_context.md)).
76
76
 
77
- When authorization is successful (i.e., the corresponding rule returns `true`), nothing happens, but in case of an authorization failure `ActionPolicy::Unauthorized` error is raised.
77
+ When authorization is successful (i.e., the corresponding rule returns `true`), nothing happens, but in case of an authorization failure `ActionPolicy::Unauthorized` error is raised:
78
+
79
+ ```ruby
80
+ rescue_from ActionPolicy::Unauthorized do |ex|
81
+ # Exception object contains the following information
82
+ ex.policy #=> policy class, e.g. UserPolicy
83
+ ex.rule #=> applied rule, e.g. :show?
84
+ end
85
+ ```
78
86
 
79
87
  There is also an `allowed_to?` method which returns `true` or `false` and could be used, for example, in views:
80
88
 
data/docs/reasons.md CHANGED
@@ -17,16 +17,23 @@ class ApplicantPolicy < ApplicationPolicy
17
17
  end
18
18
  ```
19
19
 
20
- When `ApplicantPolicy#show?` check fails, the exception has the `reasons` object, which contains additional information about the failure:
20
+ When `ApplicantPolicy#show?` check fails, the exception has the `result` object, which in its turn contains additional information about the failure (`reasons`):
21
21
 
22
22
  ```ruby
23
23
  class ApplicationController < ActionController::Base
24
24
  rescue_from ActionPolicy::Unauthorized do |ex|
25
- p ex.reasons.messages #=> { stage: [:show?] }
25
+ p ex.result.reasons.details #=> { stage: [:show?] }
26
+
27
+ # or with i18n support
28
+ p ex.result.reasons.full_messages #=> ["You do not have access to the stage"]
26
29
  end
27
30
  end
28
31
  ```
29
32
 
33
+ The reason key is the corresponding policy [identifier](writing_policies.md#identifiers).
34
+
35
+ **NOTE:** `full_messages` support hasn't been released yet. See [the issue](https://github.com/palkan/action_policy/issues/15).
36
+
30
37
  You can also wrap _local_ rules into `allowed_to?` to populate reasons:
31
38
 
32
39
  ```ruby
@@ -42,12 +49,14 @@ class ApplicantPolicy < ApplicationPolicy
42
49
  end
43
50
 
44
51
  # then the reasons object could be
45
- p ex.reasons.messages #=> { applicant: [:view_applicants?] }
52
+ p ex.result.reasons.details #=> { applicant: [:view_applicants?] }
46
53
 
47
54
  # or
48
- p ex.reasons.messages #=> { stage: [:show?] }
55
+ p ex.result.reasons.details #=> { stage: [:show?] }
49
56
  ```
50
57
 
58
+
59
+
51
60
  **What is the point of failure reasons?**
52
61
 
53
62
  First, you can provide a user with helpful feedback. For example, in the above scenario, when the reason is `ApplicantPolicy#view_applicants?`, you could show the following message:
@@ -53,3 +53,39 @@ end
53
53
  ```
54
54
 
55
55
  You can also specify all the usual options (such as `with`).
56
+
57
+ ## Identifiers
58
+
59
+ Each policy class has an `identifier`, which is by default just an underscored class name:
60
+
61
+ ```ruby
62
+ class CommentPolicy < ApplicationPolicy
63
+ end
64
+
65
+ CommentPolicy.identifier #=> :comment
66
+ ```
67
+
68
+ For namespaced policies it has a form of:
69
+
70
+ ```ruby
71
+ module ActiveAdmin
72
+ class UserPolicy < ApplicationPolicy
73
+ end
74
+ end
75
+
76
+ ActiveAdmin::UserPolicy.identifier # => :"active_admin/user"
77
+ ```
78
+
79
+ You can specify your own identifier:
80
+
81
+ ```ruby
82
+ module MyVeryLong
83
+ class LongLongNamePolicy < ApplicationPolicy
84
+ self.identifier = :long_name
85
+ end
86
+ end
87
+
88
+ MyVeryLong::LongLongNamePolicy.identifier #=> :long_name
89
+ ```
90
+
91
+ Identifiers are required for some modules, such as [failure reasons tracking](reasons.md) and [i18n](i18n.md).
@@ -3,13 +3,12 @@
3
3
  module ActionPolicy
4
4
  # Raised when `authorize!` check fails
5
5
  class Unauthorized < Error
6
- attr_reader :policy, :rule, :reasons
6
+ attr_reader :policy, :rule, :result
7
7
 
8
8
  def initialize(policy, rule)
9
9
  @policy = policy.class
10
10
  @rule = rule
11
- # Reasons module could be not included
12
- @reasons = policy.reasons if policy.respond_to?(:reasons)
11
+ @result = policy.result
13
12
  end
14
13
  end
15
14
 
@@ -3,24 +3,31 @@
3
3
  module ActionPolicy
4
4
  module Ext
5
5
  # Add Module#namespace method
6
- module ModuleNamespace
7
- refine Module do
8
- unless "".respond_to?(:safe_constantize)
9
- require "action_policy/ext/string_constantize"
10
- using ActionPolicy::Ext::StringConstantize
11
- end
6
+ module ModuleNamespace # :nodoc: all
7
+ unless "".respond_to?(:safe_constantize)
8
+ require "action_policy/ext/string_constantize"
9
+ using ActionPolicy::Ext::StringConstantize
10
+ end
12
11
 
13
- unless "".respond_to?(:match?)
14
- require "action_policy/ext/string_match"
15
- using ActionPolicy::Ext::StringMatch
16
- end
12
+ unless "".respond_to?(:match?)
13
+ require "action_policy/ext/string_match"
14
+ using ActionPolicy::Ext::StringMatch
15
+ end
17
16
 
17
+ module Ext
18
18
  def namespace
19
19
  return unless name.match?(/[^^]::/)
20
20
 
21
21
  name.sub(/::[^:]+$/, "").safe_constantize
22
22
  end
23
23
  end
24
+
25
+ # See https://github.com/jruby/jruby/issues/5220
26
+ ::Module.include(Ext) if RUBY_PLATFORM =~ /java/i
27
+
28
+ refine Module do
29
+ include Ext
30
+ end
24
31
  end
25
32
  end
26
33
  end
@@ -9,8 +9,8 @@ module ActionPolicy
9
9
  # For other core classes returns string representation.
10
10
  #
11
11
  # Raises ArgumentError otherwise.
12
- module PolicyCacheKey
13
- refine Object do
12
+ module PolicyCacheKey # :nodoc: all
13
+ module ObjectExt
14
14
  def _policy_cache_key(use_object_id: false)
15
15
  return policy_cache_key if respond_to?(:policy_cache_key)
16
16
  return cache_key if respond_to?(:cache_key)
@@ -21,6 +21,14 @@ module ActionPolicy
21
21
  end
22
22
  end
23
23
 
24
+ # JRuby doesn't support _global_ modules refinements (see https://github.com/jruby/jruby/issues/5220)
25
+ # Fallback to monkey-patching.
26
+ ::Object.include(ObjectExt) if RUBY_PLATFORM =~ /java/i
27
+
28
+ refine Object do
29
+ include ObjectExt
30
+ end
31
+
24
32
  refine NilClass do
25
33
  def _policy_cache_key(*)
26
34
  ""
@@ -51,9 +59,23 @@ module ActionPolicy
51
59
  end
52
60
  end
53
61
 
54
- refine Numeric do
55
- def _policy_cache_key(*)
56
- to_s
62
+ if RUBY_PLATFORM =~ /java/i
63
+ refine Integer do
64
+ def _policy_cache_key(*)
65
+ to_s
66
+ end
67
+ end
68
+
69
+ refine Float do
70
+ def _policy_cache_key(*)
71
+ to_s
72
+ end
73
+ end
74
+ else
75
+ refine Numeric do
76
+ def _policy_cache_key(*)
77
+ to_s
78
+ end
57
79
  end
58
80
  end
59
81
 
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionPolicy
4
+ module Ext
5
+ # Add underscore to String
6
+ module StringUnderscore
7
+ refine String do
8
+ def underscore
9
+ word = gsub(/::/, "/")
10
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
11
+ word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
12
+ word.downcase!
13
+ word
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionPolicy
4
+ module Ext # :nodoc: all
5
+ # Add yield_self and then if missing
6
+ module YieldSelfThen
7
+ module Ext
8
+ unless nil.respond_to?(:yield_self)
9
+ def yield_self
10
+ yield self
11
+ end
12
+ end
13
+
14
+ alias then yield_self
15
+ end
16
+
17
+ # See https://github.com/jruby/jruby/issues/5220
18
+ ::Object.include(Ext) if RUBY_PLATFORM =~ /java/i
19
+
20
+ refine Object do
21
+ include Ext
22
+ end
23
+ end
24
+ end
25
+ end
@@ -7,7 +7,7 @@ module ActionPolicy
7
7
  #
8
8
  # class ApplicationPolicy
9
9
  # include ActionPolicy::Policy::Core
10
- # prepend ActionPolicy::Policy::Aliases
10
+ # include ActionPolicy::Policy::Aliases
11
11
  #
12
12
  # # define which rule to use if `authorize!` called with
13
13
  # # unknown rule
@@ -23,19 +23,14 @@ module ActionPolicy
23
23
  DEFAULT = :__default__
24
24
 
25
25
  class << self
26
- def prepended(base)
26
+ def included(base)
27
27
  base.extend ClassMethods
28
- base.prepend InstanceMethods
29
28
  end
30
-
31
- alias included prepended
32
29
  end
33
30
 
34
- module InstanceMethods # :nodoc:
35
- def resolve_rule(activity)
36
- return activity if respond_to?(activity)
37
- self.class.lookup_alias(activity) || super
38
- end
31
+ def resolve_rule(activity)
32
+ return activity if respond_to?(activity)
33
+ self.class.lookup_alias(activity) || super
39
34
  end
40
35
 
41
36
  module ClassMethods # :nodoc:
@@ -36,34 +36,29 @@ module ActionPolicy
36
36
  # ApplicantPolicy.new(user: user, account: account)
37
37
  module Authorization
38
38
  class << self
39
- def prepended(base)
39
+ def included(base)
40
40
  base.extend ClassMethods
41
- base.prepend InstanceMethods
42
41
  end
43
-
44
- alias included prepended
45
42
  end
46
43
 
47
44
  attr_reader :authorization_context
48
45
 
49
- module InstanceMethods # :nodoc:
50
- def initialize(*args, **params)
51
- super(*args)
52
-
53
- @authorization_context = {}
46
+ def initialize(*args, **params)
47
+ super(*args)
54
48
 
55
- self.class.authorization_targets.each do |id, opts|
56
- raise AuthorizationContextMissing, id unless params.key?(id)
49
+ @authorization_context = {}
57
50
 
58
- val = params.fetch(id)
51
+ self.class.authorization_targets.each do |id, opts|
52
+ raise AuthorizationContextMissing, id unless params.key?(id)
59
53
 
60
- raise AuthorizationContextMissing, id if val.nil? && opts[:allow_nil] != true
54
+ val = params.fetch(id)
61
55
 
62
- authorization_context[id] = instance_variable_set("@#{id}", val)
63
- end
56
+ raise AuthorizationContextMissing, id if val.nil? && opts[:allow_nil] != true
64
57
 
65
- authorization_context.freeze
58
+ authorization_context[id] = instance_variable_set("@#{id}", val)
66
59
  end
60
+
61
+ authorization_context.freeze
67
62
  end
68
63
 
69
64
  module ClassMethods # :nodoc:
@@ -6,7 +6,10 @@ module ActionPolicy # :nodoc:
6
6
  # By default cache namespace (or prefix) contains major and minor version of the gem
7
7
  CACHE_NAMESPACE = "acp:#{ActionPolicy::VERSION.split('.').take(2).join('.')}"
8
8
 
9
+ require "action_policy/ext/yield_self_then"
9
10
  require "action_policy/ext/policy_cache_key"
11
+
12
+ using ActionPolicy::Ext::YieldSelfThen
10
13
  using ActionPolicy::Ext::PolicyCacheKey
11
14
 
12
15
  module Policy
@@ -15,12 +18,9 @@ module ActionPolicy # :nodoc:
15
18
  # NOTE: if cache_store is nil then we silently skip all the caching.
16
19
  module Cache
17
20
  class << self
18
- def prepended(base)
21
+ def included(base)
19
22
  base.extend ClassMethods
20
- base.prepend InstanceMethods
21
23
  end
22
-
23
- alias included prepended
24
24
  end
25
25
 
26
26
  def cache_namespace
@@ -36,19 +36,26 @@ module ActionPolicy # :nodoc:
36
36
  authorization_context.map { |_k, v| v._policy_cache_key.to_s }.join("/")
37
37
  end
38
38
 
39
+ # rubocop: disable Metrics/AbcSize
39
40
  def apply_with_cache(rule)
40
41
  options = self.class.cached_rules.fetch(rule)
42
+ key = cache_key(rule)
41
43
 
42
- ActionPolicy.cache_store.fetch(cache_key(rule), options) { yield }
44
+ ActionPolicy.cache_store.then do |store|
45
+ @result = store.read(key)
46
+ next result.value unless result.nil?
47
+ yield
48
+ store.write(key, result, options)
49
+ result.value
50
+ end
43
51
  end
52
+ # rubocop: enable Metrics/AbcSize
44
53
 
45
- module InstanceMethods # :nodoc:
46
- def apply(rule)
47
- return super if ActionPolicy.cache_store.nil? ||
48
- !self.class.cached_rules.key?(rule)
54
+ def apply(rule)
55
+ return super if ActionPolicy.cache_store.nil? ||
56
+ !self.class.cached_rules.key?(rule)
49
57
 
50
- apply_with_cache(rule) { super }
51
- end
58
+ apply_with_cache(rule) { super }
52
59
  end
53
60
 
54
61
  module ClassMethods # :nodoc:
@@ -7,21 +7,19 @@ module ActionPolicy
7
7
  # When you call `apply` twice on the same policy and for the same rule,
8
8
  # the check (and pre-checks) is only called once.
9
9
  module CachedApply
10
- class << self
11
- def prepended(base)
12
- base.prepend InstanceMethods
10
+ def apply(rule)
11
+ @__rules_cache__ ||= {}
12
+
13
+ if @__rules_cache__.key?(rule)
14
+ @result = @__rules_cache__[rule]
15
+ return result.value
13
16
  end
14
17
 
15
- alias included prepended
16
- end
18
+ super
17
19
 
18
- module InstanceMethods # :nodoc:
19
- def apply(rule)
20
- @__rules_cache__ ||= {}
21
- return @__rules_cache__[rule] if @__rules_cache__.key?(rule)
20
+ @__rules_cache__[rule] = result
22
21
 
23
- @__rules_cache__[rule] = super
24
- end
22
+ result.value
25
23
  end
26
24
  end
27
25
  end
@@ -1,6 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "action_policy/behaviours/policy_for"
4
+ require "action_policy/policy/execution_result"
5
+
6
+ unless "".respond_to?(:underscore)
7
+ require "action_policy/ext/string_underscore"
8
+ using ActionPolicy::Ext::StringUnderscore
9
+ end
4
10
 
5
11
  module ActionPolicy
6
12
  # Raised when `resolve_rule` failed to find an approriate
@@ -18,35 +24,80 @@ module ActionPolicy
18
24
  module Policy
19
25
  # Core policy API
20
26
  module Core
27
+ class << self
28
+ def included(base)
29
+ base.extend ClassMethods
30
+
31
+ # Generate a new class for each _policy chain_
32
+ # in order to extend it independently
33
+ base.module_eval do
34
+ @result_class = Class.new(ExecutionResult)
35
+
36
+ # we need to make this class _named_,
37
+ # 'cause anonymous classes couldn't be marshalled
38
+ base.const_set(:APR, @result_class)
39
+ end
40
+ end
41
+ end
42
+
43
+ module ClassMethods # :nodoc:
44
+ attr_writer :identifier
45
+
46
+ def result_class
47
+ return @result_class if instance_variable_defined?(:@result_class)
48
+ @result_class = superclass.result_class
49
+ end
50
+
51
+ def identifier
52
+ return @identifier if instance_variable_defined?(:@identifier)
53
+
54
+ @identifier = name.sub(/Policy$/, "").underscore.to_sym
55
+ end
56
+ end
57
+
21
58
  include ActionPolicy::Behaviours::PolicyFor
22
59
 
23
- attr_reader :record
60
+ attr_reader :record, :result
24
61
 
25
62
  def initialize(record = nil)
26
63
  @record = record
27
64
  end
28
65
 
29
- # Returns a result of applying the specified rule.
66
+ # Returns a result of applying the specified rule (true of false).
30
67
  # Unlike simply calling a predicate rule (`policy.manage?`),
31
68
  # `apply` also calls pre-checks.
32
69
  def apply(rule)
70
+ @result = self.class.result_class.new
71
+ @result.load __apply__(rule)
72
+ end
73
+
74
+ # This method performs the rule call.
75
+ # Override or extend it to provide custom functionality
76
+ # (such as caching, pre checks, etc.)
77
+ def __apply__(rule)
33
78
  public_send(rule)
34
79
  end
35
80
 
81
+ # Wrap code that could modify result
82
+ # to prevent the current result modification
83
+ def with_clean_result
84
+ was_result = @result
85
+ res = yield
86
+ @result = was_result
87
+ res
88
+ end
89
+
36
90
  # Returns a result of applying the specified rule to the specified record.
37
91
  # Under the hood a policy class for record is resolved
38
92
  # (unless it's explicitly set through `with` option).
39
93
  #
40
94
  # If record is `nil` then we uses the current policy.
41
95
  def allowed_to?(rule, record = :__undef__, **options)
42
- policy =
43
- if record == :__undef__
44
- self
45
- else
46
- policy_for(record: record, **options)
47
- end
48
-
49
- policy.apply(rule)
96
+ if record == :__undef__
97
+ __apply__(rule)
98
+ else
99
+ policy_for(record: record, **options).apply(rule)
100
+ end
50
101
  end
51
102
 
52
103
  # Returns a rule name (policy method name) for activity.
@@ -0,0 +1,26 @@
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
11
+
12
+ # Populate the final value
13
+ def load(value)
14
+ @value = value
15
+ end
16
+
17
+ def success?
18
+ @value == true
19
+ end
20
+
21
+ def fail?
22
+ @value == false
23
+ end
24
+ end
25
+ end
26
+ end
@@ -83,8 +83,8 @@ module ActionPolicy
83
83
  def call(policy)
84
84
  Result.new(policy.send(name)).tap do |res|
85
85
  # add denial reason if Reasons included
86
- policy.reasons.add(policy_class, name) if
87
- res.denied? && policy.respond_to?(:reasons)
86
+ policy.result.reasons.add(policy, name) if
87
+ res.denied? && policy.result.respond_to?(:reasons)
88
88
  end
89
89
  end
90
90
 
@@ -136,12 +136,9 @@ module ActionPolicy
136
136
  end
137
137
 
138
138
  class << self
139
- def prepended(base)
139
+ def included(base)
140
140
  base.extend ClassMethods
141
- base.prepend InstanceMethods
142
141
  end
143
-
144
- alias included prepended
145
142
  end
146
143
 
147
144
  def run_pre_checks(rule)
@@ -162,10 +159,8 @@ module ActionPolicy
162
159
  Check::Result::ALLOW
163
160
  end
164
161
 
165
- module InstanceMethods # :nodoc:
166
- def apply(rule)
167
- run_pre_checks(rule) { super }
168
- end
162
+ def __apply__(rule)
163
+ run_pre_checks(rule) { super }
169
164
  end
170
165
 
171
166
  module ClassMethods # :nodoc:
@@ -2,28 +2,33 @@
2
2
 
3
3
  module ActionPolicy
4
4
  module Policy
5
- class FailureReason # :nodoc:
6
- attr_reader :policy, :rule
5
+ # Failures reasons store
6
+ class FailureReasons
7
+ attr_reader :details
7
8
 
8
- def initialize(policy_or_class, rule)
9
- @policy = policy_or_class.is_a?(Class) ? policy_or_class : policy_or_class.class
10
- @rule = rule
9
+ def initialize
10
+ @details = {}
11
11
  end
12
- end
13
12
 
14
- # Failures reasons store
15
- class FailureReasons
16
- include Enumerable
17
- extend Forwardable
13
+ def add(policy_or_class, rule)
14
+ policy_class = policy_or_class.is_a?(Class) ? policy_or_class : policy_or_class.class
15
+ details[policy_class.identifier] ||= []
16
+ details[policy_class.identifier] << rule
17
+ end
18
18
 
19
- def_delegators :@reasons, :size, :empty?, :last, :each
19
+ def empty?
20
+ details.empty?
21
+ end
20
22
 
21
- def initialize
22
- @reasons = []
23
+ def present?
24
+ !empty?
23
25
  end
26
+ end
24
27
 
25
- def add(policy, rule)
26
- @reasons << FailureReason.new(policy, rule)
28
+ # Extend ExecutionResult with `reasons` method
29
+ module ResultFailureReasons
30
+ def reasons
31
+ @reasons ||= FailureReasons.new
27
32
  end
28
33
  end
29
34
 
@@ -45,9 +50,21 @@ module ActionPolicy
45
50
  # information about the failure:
46
51
  #
47
52
  # rescue_from ActionPolicy::Unauthorized do |ex|
48
- # ex.reasons.messages #=> { stage: [:show] }
53
+ # ex.policy #=> ApplicantPolicy
54
+ # ex.rule #=> :show?
55
+ # ex.result.reasons.details #=> { stage: [:show?] }
56
+ # end
57
+ #
58
+ # NOTE: the reason key (`stage`) is a policy identifier (underscored class name by default).
59
+ # For namespaced policies it has a form of:
60
+ #
61
+ # class Admin::UserPolicy < ApplicationPolicy
62
+ # # ..
49
63
  # end
50
64
  #
65
+ # reasons.details #=> { :"admin/user" => [:show?] }
66
+ #
67
+ #
51
68
  # You can also wrap _local_ rules into `allowed_to?` to populate reasons:
52
69
  #
53
70
  # class ApplicantPolicy < ApplicationPolicy
@@ -62,48 +79,29 @@ module ActionPolicy
62
79
  # end
63
80
  module Reasons
64
81
  class << self
65
- def prepended(base)
66
- base.prepend InstanceMethods
82
+ def included(base)
83
+ base.result_class.include(ResultFailureReasons)
67
84
  end
68
-
69
- alias included prepended
70
85
  end
71
86
 
72
- attr_reader :reasons
87
+ # rubocop: disable Metrics/MethodLength
88
+ def allowed_to?(rule, record = :__undef__, **options)
89
+ policy = nil
73
90
 
74
- def with_clean_reasons # :nodoc:
75
- old_reasons = reasons
76
- @reasons = nil
77
- res = yield
78
- @reasons = old_reasons
79
- res
80
- end
91
+ succeed =
92
+ if record == :__undef__
93
+ policy = self
94
+ with_clean_result { apply(rule) }
95
+ else
96
+ policy = policy_for(record: record, **options)
81
97
 
82
- module InstanceMethods # :nodoc:
83
- def apply(rule)
84
- @reasons = FailureReasons.new
85
- super
86
- end
87
-
88
- # rubocop: disable Metrics/MethodLength
89
- def allowed_to?(rule, record = :__undef__, **options)
90
- policy = nil
91
-
92
- succeed =
93
- if record == :__undef__
94
- policy = self
95
- with_clean_reasons { apply(rule) }
96
- else
97
- policy = policy_for(record: record, **options)
98
+ policy.apply(rule)
99
+ end
98
100
 
99
- policy.apply(rule)
100
- end
101
-
102
- reasons.add(policy, rule) if reasons && !succeed
103
- succeed
104
- end
105
- # rubocop: enable Metrics/MethodLength
101
+ result.reasons.add(policy, rule) unless succeed
102
+ succeed
106
103
  end
104
+ # rubocop: enable Metrics/MethodLength
107
105
  end
108
106
  end
109
107
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionPolicy
4
- VERSION = "0.1.4"
4
+ VERSION = "0.2.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: action_policy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Dementyev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-06-06 00:00:00.000000000 Z
11
+ date: 2018-06-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -163,6 +163,8 @@ files:
163
163
  - lib/action_policy/ext/policy_cache_key.rb
164
164
  - lib/action_policy/ext/string_constantize.rb
165
165
  - lib/action_policy/ext/string_match.rb
166
+ - lib/action_policy/ext/string_underscore.rb
167
+ - lib/action_policy/ext/yield_self_then.rb
166
168
  - lib/action_policy/lookup_chain.rb
167
169
  - lib/action_policy/policy/aliases.rb
168
170
  - lib/action_policy/policy/authorization.rb
@@ -170,6 +172,7 @@ files:
170
172
  - lib/action_policy/policy/cached_apply.rb
171
173
  - lib/action_policy/policy/core.rb
172
174
  - lib/action_policy/policy/defaults.rb
175
+ - lib/action_policy/policy/execution_result.rb
173
176
  - lib/action_policy/policy/pre_check.rb
174
177
  - lib/action_policy/policy/reasons.rb
175
178
  - lib/action_policy/rails/channel.rb